@hatchway/cli 0.50.68 → 0.50.70

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.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // Hatchway CLI - Built with Rollup
2
- import * as Sentry from '@sentry/node';
3
2
  import { query } from '@anthropic-ai/claude-agent-sdk';
4
3
  import { Codex } from '@openai/codex-sdk';
5
4
  import { a as streamLog, f as fileLog, i as initRunnerLogger, s as setFileLoggerTuiMode, b as getLogger } from './chunks/runner-logger-instance-Dj_JMznn.js';
@@ -9,13 +8,13 @@ import { fileURLToPath } from 'node:url';
9
8
  import { existsSync, readdirSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
9
  import { readFile } from 'fs/promises';
11
10
  import * as path from 'path';
12
- import { join as join$1 } from 'path';
11
+ import { join as join$1, resolve as resolve$1, relative as relative$1, isAbsolute as isAbsolute$1 } from 'path';
13
12
  import WebSocket$1, { WebSocketServer, WebSocket } from 'ws';
14
13
  import { drizzle } from 'drizzle-orm/node-postgres';
15
14
  import pg from 'pg';
16
15
  import { pgTable, timestamp, boolean, text, uuid, index, uniqueIndex, integer, jsonb } from 'drizzle-orm/pg-core';
17
16
  import { sql, eq, and, desc, isNull } from 'drizzle-orm';
18
- import { randomUUID, createHash } from 'crypto';
17
+ import { randomUUID, timingSafeEqual, createHash } from 'crypto';
19
18
  import { migrate } from 'drizzle-orm/node-postgres/migrator';
20
19
  import { z } from 'zod';
21
20
  import { spawn } from 'node:child_process';
@@ -28,8 +27,7 @@ import { readFile as readFile$1, rm, writeFile, readdir } from 'node:fs/promises
28
27
  import { simpleGit } from 'simple-git';
29
28
  import * as os from 'os';
30
29
  import { existsSync as existsSync$1, mkdirSync as mkdirSync$1 } from 'fs';
31
- import { tunnelManager } from './chunks/manager-DjVI7erc.js';
32
- import { a as getPackageVersion } from './chunks/version-info-CDtU8Ta2.js';
30
+ import { tunnelManager } from './chunks/manager-0U0BIO9r.js';
33
31
  import 'chalk';
34
32
  import 'http';
35
33
  import 'http-proxy';
@@ -183,14 +181,6 @@ function getPlatformPluginDir() {
183
181
  if (process.env.SILENT_MODE !== '1') {
184
182
  process.stderr.write('[skills] Platform plugin directory not found\n');
185
183
  }
186
- Sentry.logger.error('Platform plugin directory not found — agent will run without core skills', {
187
- candidatePaths: [
188
- join(__dirname$3, 'skills', 'platform-plugin'),
189
- join(__dirname$3, 'lib', 'skills', 'platform-plugin'),
190
- join(__dirname$3, '..', 'src', 'lib', 'skills', 'platform-plugin'),
191
- ].join(', '),
192
- __dirname: __dirname$3,
193
- });
194
184
  }
195
185
  }
196
186
  return _pluginDir;
@@ -277,12 +267,12 @@ var init_tags$1 = __esm$1({
277
267
  sdk: "agent"
278
268
  },
279
269
  {
280
- value: "claude-opus-4-6",
281
- label: "Claude Opus 4.6",
270
+ value: "claude-opus-4-8",
271
+ label: "Claude Opus 4.8",
282
272
  description: "Anthropic Claude - Most capable for complex tasks",
283
273
  logo: "/claude.png",
284
274
  provider: "claude-code",
285
- model: "claude-opus-4-6",
275
+ model: "claude-opus-4-8",
286
276
  sdk: "agent"
287
277
  },
288
278
  {
@@ -306,10 +296,10 @@ var init_tags$1 = __esm$1({
306
296
  {
307
297
  value: "factory-droid-opus",
308
298
  label: "Factory Droid (Opus)",
309
- description: "Factory Droid SDK with Claude Opus 4.6",
299
+ description: "Factory Droid SDK with Claude Opus 4.8",
310
300
  logo: "/factory.svg",
311
301
  provider: "factory-droid",
312
- model: "claude-opus-4-6-20251101",
302
+ model: "claude-opus-4-8",
313
303
  sdk: "droid"
314
304
  },
315
305
  {
@@ -1539,7 +1529,10 @@ var DEFAULT_OPENCODE_MODEL_ID = "anthropic/claude-sonnet-4-6";
1539
1529
  var LEGACY_MODEL_MAP = {
1540
1530
  "claude-haiku-4-5": "anthropic/claude-haiku-4-5",
1541
1531
  "claude-sonnet-4-6": "anthropic/claude-sonnet-4-6",
1542
- "claude-opus-4-6": "anthropic/claude-opus-4-6",
1532
+ "claude-opus-4-8": "anthropic/claude-opus-4-8",
1533
+ // Legacy aliases — upgrade previously-saved selections to the current model
1534
+ "claude-opus-4-6": "anthropic/claude-opus-4-8",
1535
+ "claude-opus-4-6-20251101": "anthropic/claude-opus-4-8",
1543
1536
  "gpt-5-codex": "openai/gpt-5.2-codex",
1544
1537
  "gpt-5.2-codex": "openai/gpt-5.2-codex"
1545
1538
  };
@@ -1568,8 +1561,8 @@ var CLAUDE_MODEL_METADATA = {
1568
1561
  description: "Balanced performance and quality",
1569
1562
  provider: "anthropic"
1570
1563
  },
1571
- "claude-opus-4-6": {
1572
- label: "Claude Opus 4.6",
1564
+ "claude-opus-4-8": {
1565
+ label: "Claude Opus 4.8",
1573
1566
  description: "Most capable Claude model for complex tasks",
1574
1567
  provider: "anthropic"
1575
1568
  }
@@ -1585,8 +1578,8 @@ var MODEL_METADATA = {
1585
1578
  description: "Balanced performance and quality",
1586
1579
  provider: "anthropic"
1587
1580
  },
1588
- "anthropic/claude-opus-4-6": {
1589
- label: "Claude Opus 4.6",
1581
+ "anthropic/claude-opus-4-8": {
1582
+ label: "Claude Opus 4.8",
1590
1583
  description: "Most capable for complex tasks",
1591
1584
  provider: "anthropic"
1592
1585
  },
@@ -1683,6 +1676,8 @@ async function resolveAgentStrategy$1(agentId) {
1683
1676
 
1684
1677
  // src/index.ts
1685
1678
  init_config_server$1();
1679
+
1680
+ // src/lib/logging/build-logger.ts
1686
1681
  var BuildLogger$1 = class BuildLogger {
1687
1682
  buildId = null;
1688
1683
  projectId = null;
@@ -1733,23 +1728,6 @@ var BuildLogger$1 = class BuildLogger {
1733
1728
  } else {
1734
1729
  logFn(`${icon} ${prefix} ${message}`);
1735
1730
  }
1736
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
1737
- try {
1738
- if (level === "warn" || level === "error") {
1739
- Sentry.addBreadcrumb({
1740
- category: `build-logger.${context}`,
1741
- message,
1742
- level: level === "error" ? "error" : "warning",
1743
- data: {
1744
- ...data,
1745
- buildId: this.buildId,
1746
- projectId: this.projectId
1747
- }
1748
- });
1749
- }
1750
- } catch {
1751
- }
1752
- }
1753
1731
  }
1754
1732
  /**
1755
1733
  * Orchestrator-specific logging methods
@@ -2269,6 +2247,17 @@ var projects = pgTable("projects", {
2269
2247
  // 'deploying' | 'success' | 'failed' | 'crashed'
2270
2248
  railwayLastDeployedAt: timestamp("railway_last_deployed_at"),
2271
2249
  // Last successful deployment
2250
+ // Execution mode: where builds run for this project
2251
+ executionMode: text("execution_mode").default("local"),
2252
+ // 'local' | 'sandbox'
2253
+ sandboxId: text("sandbox_id"),
2254
+ // Railway sandbox id when executionMode='sandbox' and currently running (warm)
2255
+ sandboxStatus: text("sandbox_status"),
2256
+ // 'provisioning' | 'running' | 'stopped' | 'failed'
2257
+ sandboxCheckpoint: text("sandbox_checkpoint"),
2258
+ // Railway checkpoint key for this project's saved workspace (restore point)
2259
+ sandboxSubdomain: text("sandbox_subdomain"),
2260
+ // Stable railgate subdomain so the preview URL persists across restarts
2272
2261
  createdAt: timestamp("created_at").notNull().defaultNow(),
2273
2262
  updatedAt: timestamp("updated_at").notNull().defaultNow()
2274
2263
  }, (table) => ({
@@ -3087,8 +3076,18 @@ var RunnerCommandQueue = class {
3087
3076
  }
3088
3077
  };
3089
3078
  var commandQueue = new RunnerCommandQueue();
3079
+ function timingSafeEqualString(a, b) {
3080
+ const bufA = Buffer.from(a);
3081
+ const bufB = Buffer.from(b);
3082
+ if (bufA.length !== bufB.length) {
3083
+ return false;
3084
+ }
3085
+ return timingSafeEqual(bufA, bufB);
3086
+ }
3090
3087
 
3091
3088
  // src/lib/websocket/server.ts
3089
+ var isLocalMode = () => process.env.HATCHWAY_LOCAL_MODE === "true";
3090
+ var upgradeAuthContext = /* @__PURE__ */ new WeakMap();
3092
3091
  var getSharedSecret = () => process.env.RUNNER_SHARED_SECRET;
3093
3092
  function hashRunnerKey(key) {
3094
3093
  return createHash("sha256").update(key).digest("hex");
@@ -3143,6 +3142,18 @@ var BuildWebSocketServer = class {
3143
3142
  initialized = false;
3144
3143
  // Callback for runner status changes (set by app layer)
3145
3144
  onRunnerStatusChangeCallback = null;
3145
+ // Client auth hooks (set by app layer). Without these, client connections are
3146
+ // only accepted in local mode - default deny in hosted deployments.
3147
+ authenticateClient = null;
3148
+ authorizeProjectAccess = null;
3149
+ /**
3150
+ * Install authentication hooks for frontend client connections.
3151
+ * Must be called by the app layer before clients connect in hosted mode.
3152
+ */
3153
+ setClientAuth(authenticate, authorize) {
3154
+ this.authenticateClient = authenticate;
3155
+ this.authorizeProjectAccess = authorize;
3156
+ }
3146
3157
  constructor() {
3147
3158
  buildLogger$2.websocket.serverCreated(this.instanceId);
3148
3159
  }
@@ -3162,7 +3173,13 @@ var BuildWebSocketServer = class {
3162
3173
  // Disable compression for lower latency
3163
3174
  });
3164
3175
  this.wss.on("connection", (ws, req) => {
3165
- this.handleConnection(ws, req);
3176
+ this.handleConnection(ws, req).catch((error) => {
3177
+ buildLogger$2.websocket.error("Failed to handle client connection", error);
3178
+ try {
3179
+ ws.close(1011, "Internal error");
3180
+ } catch {
3181
+ }
3182
+ });
3166
3183
  });
3167
3184
  this.runnerWss = new WebSocketServer({
3168
3185
  noServer: true,
@@ -3178,8 +3195,20 @@ var BuildWebSocketServer = class {
3178
3195
  this.runnerWss.emit("connection", ws, request);
3179
3196
  });
3180
3197
  } else if (pathname === path || pathname === "/ws") {
3181
- this.wss.handleUpgrade(request, socket, head, (ws) => {
3182
- this.wss.emit("connection", ws, request);
3198
+ this.resolveClientAuth(request).then((auth) => {
3199
+ if (!auth.allowed) {
3200
+ buildLogger$2.log("warn", "websocket", "Rejected unauthenticated client upgrade");
3201
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
3202
+ socket.destroy();
3203
+ return;
3204
+ }
3205
+ upgradeAuthContext.set(request, { userId: auth.userId });
3206
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
3207
+ this.wss.emit("connection", ws, request);
3208
+ });
3209
+ }).catch((error) => {
3210
+ buildLogger$2.websocket.error("Client upgrade auth failed", error);
3211
+ socket.destroy();
3183
3212
  });
3184
3213
  } else {
3185
3214
  if (pathname && pathname !== "/") {
@@ -3205,19 +3234,58 @@ var BuildWebSocketServer = class {
3205
3234
  // ============================================================
3206
3235
  // FRONTEND CLIENT HANDLING
3207
3236
  // ============================================================
3237
+ /**
3238
+ * Authenticate a client upgrade request.
3239
+ * Local mode: always allowed. Hosted mode: requires the app layer to have
3240
+ * installed an authenticator via setClientAuth() - otherwise deny.
3241
+ */
3242
+ async resolveClientAuth(req) {
3243
+ if (isLocalMode()) {
3244
+ return { allowed: true };
3245
+ }
3246
+ if (!this.authenticateClient) {
3247
+ buildLogger$2.log("warn", "websocket", "No client authenticator configured - denying client connection");
3248
+ return { allowed: false };
3249
+ }
3250
+ const auth = await this.authenticateClient(req);
3251
+ if (!auth) {
3252
+ return { allowed: false };
3253
+ }
3254
+ return { allowed: true, userId: auth.userId };
3255
+ }
3256
+ /**
3257
+ * Check whether a user may access a project. Local mode always allows.
3258
+ */
3259
+ async canAccessProject(userId, projectId) {
3260
+ if (isLocalMode()) return true;
3261
+ if (!this.authorizeProjectAccess || !userId) return false;
3262
+ try {
3263
+ return await this.authorizeProjectAccess(userId, projectId);
3264
+ } catch (error) {
3265
+ buildLogger$2.websocket.error("Project access check failed", error, { projectId });
3266
+ return false;
3267
+ }
3268
+ }
3208
3269
  /**
3209
3270
  * Handle new frontend WebSocket connection
3210
3271
  */
3211
- handleConnection(ws, req) {
3272
+ async handleConnection(ws, req) {
3212
3273
  const clientId = this.generateClientId();
3213
3274
  const url = new URL(req.url || "", `http://${req.headers.host}`);
3214
3275
  const projectId = url.searchParams.get("projectId") || "";
3215
3276
  const sessionId = url.searchParams.get("sessionId") || void 0;
3277
+ const userId = upgradeAuthContext.get(req)?.userId;
3278
+ if (projectId && !await this.canAccessProject(userId, projectId)) {
3279
+ buildLogger$2.log("warn", "websocket", `Client denied access to project ${projectId}`);
3280
+ ws.close(4403, "Forbidden");
3281
+ return;
3282
+ }
3216
3283
  buildLogger$2.websocket.clientConnected(clientId, projectId, sessionId);
3217
3284
  this.clients.set(clientId, {
3218
3285
  ws,
3219
3286
  projectId,
3220
3287
  sessionId,
3288
+ userId,
3221
3289
  lastHeartbeat: Date.now()
3222
3290
  });
3223
3291
  this.sendMessage(ws, {
@@ -3230,7 +3298,9 @@ var BuildWebSocketServer = class {
3230
3298
  ws.on("message", (data) => {
3231
3299
  try {
3232
3300
  const message = JSON.parse(data.toString());
3233
- this.handleClientMessage(clientId, message);
3301
+ this.handleClientMessage(clientId, message).catch((error) => {
3302
+ buildLogger$2.websocket.error("Failed to handle client message", error, { clientId });
3303
+ });
3234
3304
  } catch (error) {
3235
3305
  buildLogger$2.websocket.error("Failed to parse client message", error, { clientId });
3236
3306
  }
@@ -3267,7 +3337,7 @@ var BuildWebSocketServer = class {
3267
3337
  const result = await validateRunnerKey(token);
3268
3338
  isAuthenticated = result.valid;
3269
3339
  runnerUserId = result.userId;
3270
- } else if (sharedSecret && token === sharedSecret) {
3340
+ } else if (sharedSecret && timingSafeEqualString(token, sharedSecret)) {
3271
3341
  isAuthenticated = true;
3272
3342
  }
3273
3343
  }
@@ -3283,6 +3353,19 @@ var BuildWebSocketServer = class {
3283
3353
  }
3284
3354
  const url = new URL(req.url || "", `http://${req.headers.host}`);
3285
3355
  const runnerId = url.searchParams.get("runnerId") ?? "default";
3356
+ const existing = this.runnerConnections.get(runnerId);
3357
+ if (existing) {
3358
+ if (existing.userId !== runnerUserId) {
3359
+ buildLogger$2.log("warn", "websocket", `Rejected runner connection: runnerId '${runnerId}' already claimed by another user`);
3360
+ ws.close(1008, "Runner ID already in use");
3361
+ return;
3362
+ }
3363
+ clearInterval(existing.pingInterval);
3364
+ try {
3365
+ existing.socket.close(1e3, "Replaced by new connection");
3366
+ } catch {
3367
+ }
3368
+ }
3286
3369
  buildLogger$2.websocket.runnerConnected(runnerId);
3287
3370
  const pingInterval = setInterval(() => {
3288
3371
  if (ws.readyState === WebSocket.OPEN) {
@@ -3296,11 +3379,6 @@ var BuildWebSocketServer = class {
3296
3379
  pingInterval,
3297
3380
  userId: runnerUserId
3298
3381
  });
3299
- Sentry.addBreadcrumb({
3300
- category: "websocket",
3301
- message: `Runner connected: ${runnerId}`,
3302
- level: "info"
3303
- });
3304
3382
  const queueResult = commandQueue.processQueue(runnerId);
3305
3383
  if (queueResult.sent > 0 || queueResult.failed > 0) {
3306
3384
  buildLogger$2.log("info", "websocket", `Runner ${runnerId} reconnected - processed queued commands`, {
@@ -3330,16 +3408,15 @@ var BuildWebSocketServer = class {
3330
3408
  }
3331
3409
  } catch (error) {
3332
3410
  this.runnerTotalErrors++;
3333
- Sentry.captureException(error, {
3334
- tags: { runnerId, source: "websocket_message" },
3335
- level: "error"
3336
- });
3337
3411
  buildLogger$2.websocket.error("Failed to handle runner message", error, { runnerId });
3338
3412
  }
3339
3413
  });
3340
3414
  ws.on("close", (code) => {
3341
- buildLogger$2.websocket.runnerDisconnected(runnerId, code);
3342
3415
  const conn = this.runnerConnections.get(runnerId);
3416
+ if (conn && conn.socket !== ws) {
3417
+ return;
3418
+ }
3419
+ buildLogger$2.websocket.runnerDisconnected(runnerId, code);
3343
3420
  if (conn) {
3344
3421
  clearInterval(conn.pingInterval);
3345
3422
  }
@@ -3347,20 +3424,10 @@ var BuildWebSocketServer = class {
3347
3424
  httpProxyManager.cancelRequestsForRunner(runnerId);
3348
3425
  hmrProxyManager$1.disconnectRunner(runnerId);
3349
3426
  this.cleanupRunnerProcesses(runnerId);
3350
- Sentry.addBreadcrumb({
3351
- category: "websocket",
3352
- message: `Runner disconnected: ${runnerId}`,
3353
- level: "info",
3354
- data: { code }
3355
- });
3356
3427
  });
3357
3428
  ws.on("error", (error) => {
3358
3429
  buildLogger$2.websocket.error("Runner socket error", error, { runnerId });
3359
3430
  this.runnerTotalErrors++;
3360
- Sentry.captureException(error, {
3361
- tags: { runnerId, source: "websocket_error" },
3362
- level: "error"
3363
- });
3364
3431
  const conn = this.runnerConnections.get(runnerId);
3365
3432
  if (conn) {
3366
3433
  clearInterval(conn.pingInterval);
@@ -3398,30 +3465,12 @@ var BuildWebSocketServer = class {
3398
3465
  return false;
3399
3466
  }
3400
3467
  try {
3401
- const activeSpan = Sentry.getActiveSpan();
3402
- const hasTrace = !!activeSpan;
3403
- if (activeSpan) {
3404
- const traceData = Sentry.getTraceData();
3405
- if (traceData["sentry-trace"]) {
3406
- command._sentry = {
3407
- trace: traceData["sentry-trace"],
3408
- baggage: traceData.baggage
3409
- };
3410
- buildLogger$2.log("debug", "websocket", `Attaching trace to command ${command.type}`, {
3411
- tracePreview: traceData["sentry-trace"].substring(0, 50)
3412
- });
3413
- }
3414
- }
3415
- buildLogger$2.websocket.commandSent(runnerId, command.type, hasTrace);
3468
+ buildLogger$2.websocket.commandSent(runnerId, command.type, false);
3416
3469
  connection.socket.send(JSON.stringify(command));
3417
3470
  this.runnerTotalCommands++;
3418
3471
  return true;
3419
3472
  } catch (error) {
3420
3473
  this.runnerTotalErrors++;
3421
- Sentry.captureException(error, {
3422
- tags: { runnerId, commandType: command.type },
3423
- level: "error"
3424
- });
3425
3474
  buildLogger$2.websocket.error("Failed to send command to runner", error, { runnerId, commandType: command.type });
3426
3475
  return false;
3427
3476
  }
@@ -3484,12 +3533,6 @@ var BuildWebSocketServer = class {
3484
3533
  for (const [runnerId, conn] of this.runnerConnections.entries()) {
3485
3534
  if (now - conn.lastHeartbeat > this.RUNNER_HEARTBEAT_TIMEOUT) {
3486
3535
  buildLogger$2.websocket.runnerStaleRemoved(runnerId);
3487
- Sentry.addBreadcrumb({
3488
- category: "websocket",
3489
- message: `Stale runner connection removed: ${runnerId}`,
3490
- level: "warning",
3491
- data: { age: now - conn.lastHeartbeat }
3492
- });
3493
3536
  clearInterval(conn.pingInterval);
3494
3537
  conn.socket.close(1e3, "Heartbeat timeout");
3495
3538
  this.runnerConnections.delete(runnerId);
@@ -3527,12 +3570,6 @@ var BuildWebSocketServer = class {
3527
3570
  runnerId,
3528
3571
  projectIds
3529
3572
  });
3530
- Sentry.addBreadcrumb({
3531
- category: "websocket",
3532
- message: `Cleaned up processes for disconnected runner`,
3533
- level: "info",
3534
- data: { runnerId, processCount: projectIds.length }
3535
- });
3536
3573
  if (this.onRunnerStatusChangeCallback) {
3537
3574
  try {
3538
3575
  this.onRunnerStatusChangeCallback(runnerId, false, projectIds);
@@ -3542,10 +3579,6 @@ var BuildWebSocketServer = class {
3542
3579
  }
3543
3580
  } catch (error) {
3544
3581
  buildLogger$2.websocket.error("Failed to cleanup runner processes", error, { runnerId });
3545
- Sentry.captureException(error, {
3546
- tags: { runnerId, source: "runner_cleanup" },
3547
- level: "error"
3548
- });
3549
3582
  }
3550
3583
  }
3551
3584
  /**
@@ -3570,7 +3603,7 @@ var BuildWebSocketServer = class {
3570
3603
  /**
3571
3604
  * Handle messages from client (heartbeat, resubscribe, HMR, etc.)
3572
3605
  */
3573
- handleClientMessage(clientId, message) {
3606
+ async handleClientMessage(clientId, message) {
3574
3607
  const client = this.clients.get(clientId);
3575
3608
  if (!client) return;
3576
3609
  switch (message.type) {
@@ -3578,11 +3611,19 @@ var BuildWebSocketServer = class {
3578
3611
  client.lastHeartbeat = Date.now();
3579
3612
  this.sendMessage(client.ws, { type: "heartbeat-ack", timestamp: Date.now() });
3580
3613
  break;
3581
- case "subscribe":
3582
- client.projectId = message.projectId;
3614
+ case "subscribe": {
3615
+ const targetProjectId = typeof message.projectId === "string" ? message.projectId : "";
3616
+ const alreadyVerified = targetProjectId === client.projectId;
3617
+ if (targetProjectId && !alreadyVerified && !await this.canAccessProject(client.userId, targetProjectId)) {
3618
+ buildLogger$2.log("warn", "websocket", `Client ${clientId} denied subscribe to project ${targetProjectId}`);
3619
+ this.sendMessage(client.ws, { type: "error", error: "Forbidden", projectId: targetProjectId });
3620
+ break;
3621
+ }
3622
+ client.projectId = targetProjectId;
3583
3623
  client.sessionId = message.sessionId;
3584
- buildLogger$2.websocket.clientSubscribed(clientId, message.projectId);
3624
+ buildLogger$2.websocket.clientSubscribed(clientId, targetProjectId);
3585
3625
  break;
3626
+ }
3586
3627
  case "get-state":
3587
3628
  this.sendCurrentState(client);
3588
3629
  break;
@@ -3601,10 +3642,18 @@ var BuildWebSocketServer = class {
3601
3642
  /**
3602
3643
  * Handle HMR connect request from frontend
3603
3644
  */
3604
- handleHmrConnect(clientId, client, message) {
3605
- const { connectionId, port, protocol, runnerId } = message;
3606
- const targetRunnerId = runnerId || this.getRunnerIdForProject(client.projectId);
3607
- if (!targetRunnerId) {
3645
+ async handleHmrConnect(clientId, client, message) {
3646
+ const { connectionId, protocol } = message;
3647
+ if (!client.projectId) {
3648
+ this.sendMessage(client.ws, {
3649
+ type: "hmr-error",
3650
+ connectionId,
3651
+ error: "Not subscribed to a project"
3652
+ });
3653
+ return;
3654
+ }
3655
+ const target = await this.getHmrTargetForProject(client.projectId);
3656
+ if (!target) {
3608
3657
  this.sendMessage(client.ws, {
3609
3658
  type: "hmr-error",
3610
3659
  connectionId,
@@ -3612,6 +3661,7 @@ var BuildWebSocketServer = class {
3612
3661
  });
3613
3662
  return;
3614
3663
  }
3664
+ const { runnerId: targetRunnerId, port } = target;
3615
3665
  if (!client.hmrConnections) {
3616
3666
  client.hmrConnections = /* @__PURE__ */ new Set();
3617
3667
  }
@@ -3672,12 +3722,22 @@ var BuildWebSocketServer = class {
3672
3722
  hmrProxyManager$1.disconnect(connectionId);
3673
3723
  }
3674
3724
  /**
3675
- * Get the runner ID for a project (looks up from connected runners)
3676
- * In a multi-runner setup, this would query the database
3725
+ * Resolve the HMR tunnel target (runner + dev server port) for a project.
3726
+ * The project's assigned runner must be connected and its dev server port
3727
+ * recorded - no fallback to "any runner".
3677
3728
  */
3678
- getRunnerIdForProject(projectId) {
3679
- const runners = this.listRunnerConnections();
3680
- return runners.length > 0 ? runners[0].runnerId : null;
3729
+ async getHmrTargetForProject(projectId) {
3730
+ try {
3731
+ const rows = await db.select({ runnerId: projects.runnerId, devServerPort: projects.devServerPort }).from(projects).where(eq(projects.id, projectId)).limit(1);
3732
+ if (rows.length === 0) return null;
3733
+ const { runnerId, devServerPort } = rows[0];
3734
+ if (!runnerId || !devServerPort) return null;
3735
+ if (!this.runnerConnections.has(runnerId)) return null;
3736
+ return { runnerId, port: devServerPort };
3737
+ } catch (error) {
3738
+ buildLogger$2.websocket.error("Failed to resolve HMR target", error, { projectId });
3739
+ return null;
3740
+ }
3681
3741
  }
3682
3742
  /**
3683
3743
  * @deprecated Use discrete event broadcasts instead (broadcastBuildStarted, broadcastTodosUpdate,
@@ -3693,11 +3753,7 @@ var BuildWebSocketServer = class {
3693
3753
  updates: []
3694
3754
  });
3695
3755
  }
3696
- const activeSpan = Sentry.getActiveSpan();
3697
- const traceContext = activeSpan ? {
3698
- trace: Sentry.getTraceData()["sentry-trace"],
3699
- baggage: Sentry.getTraceData().baggage
3700
- } : void 0;
3756
+ const traceContext = void 0;
3701
3757
  const batch = this.pendingUpdates.get(key);
3702
3758
  batch.updates.push({
3703
3759
  type: "state-update",
@@ -3728,11 +3784,7 @@ var BuildWebSocketServer = class {
3728
3784
  updates: []
3729
3785
  });
3730
3786
  }
3731
- const activeSpan = Sentry.getActiveSpan();
3732
- const traceContext = activeSpan ? {
3733
- trace: Sentry.getTraceData()["sentry-trace"],
3734
- baggage: Sentry.getTraceData().baggage
3735
- } : void 0;
3787
+ const traceContext = void 0;
3736
3788
  const batch = this.pendingUpdates.get(key);
3737
3789
  batch.updates.push({
3738
3790
  type: "tool-call",
@@ -4333,12 +4385,6 @@ function buildPromptWithImages(prompt, messageParts) {
4333
4385
  *
4334
4386
  * With:
4335
4387
  * query() SDK function -> minimal transformation -> output
4336
- *
4337
- * Sentry Integration:
4338
- * - Manual gen_ai.* spans for AI Agent Monitoring in Sentry
4339
- * - gen_ai.invoke_agent wraps the full query lifecycle
4340
- * - gen_ai.execute_tool spans are emitted per tool call
4341
- * - Token usage and cost are captured from the SDK result message
4342
4388
  */
4343
4389
  function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortController) {
4344
4390
  return async function* nativeClaudeQuery(prompt, workingDirectory, systemPrompt, _agent, _codexThreadId, messageParts) {
@@ -4362,12 +4408,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4362
4408
  const platformPlugins = platformPluginDir
4363
4409
  ? [{ type: 'local', path: platformPluginDir }]
4364
4410
  : [];
4365
- if (platformPlugins.length === 0) {
4366
- Sentry.logger.warn('Agent starting without platform skills — degraded capabilities', {
4367
- model: modelId,
4368
- workingDirectory,
4369
- });
4370
- }
4371
4411
  // Check for multi-modal content
4372
4412
  const hasImages = messageParts?.some(p => p.type === 'image');
4373
4413
  if (hasImages) {
@@ -4430,28 +4470,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4430
4470
  let pendingRequestMessages = [
4431
4471
  { role: 'user', content: finalPrompt.substring(0, 1000) },
4432
4472
  ];
4433
- // Create the gen_ai.invoke_agent span as a child of the current active span.
4434
- //
4435
- // We use startInactiveSpan because this is an async generator — we can't use
4436
- // startSpan/startSpanManual (both require a callback, and yields can't cross
4437
- // callback boundaries). startInactiveSpan creates a span that inherits the
4438
- // parent from the current active span (build.runner, restored by engine.ts
4439
- // via Sentry.withActiveSpan).
4440
- //
4441
- // For tool spans, we use Sentry.withActiveSpan(agentSpan, ...) to temporarily
4442
- // make the agent span active so tool spans become its children.
4443
- const agentSpan = Sentry.startInactiveSpan({
4444
- op: 'gen_ai.invoke_agent',
4445
- name: 'invoke_agent hatchway-builder',
4446
- attributes: {
4447
- 'gen_ai.operation.name': 'invoke_agent',
4448
- 'gen_ai.agent.name': 'hatchway-builder',
4449
- 'gen_ai.request.model': modelId,
4450
- 'gen_ai.request.messages': JSON.stringify([{ role: 'user', content: finalPrompt.substring(0, 1000) }]),
4451
- 'gen_ai.request.available_tools': JSON.stringify(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Task', 'TodoWrite', 'WebFetch']
4452
- .map(name => ({ name, type: 'function' }))),
4453
- },
4454
- });
4455
4473
  try {
4456
4474
  // Stream messages directly from the SDK
4457
4475
  for await (const sdkMessage of query({ prompt: finalPrompt, options })) {
@@ -4465,65 +4483,17 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4465
4483
  // pendingRequestMessages) and the response text / tool calls.
4466
4484
  if (transformed.type === 'assistant' && transformed.message?.content) {
4467
4485
  turnCount++;
4468
- // Collect response text and tool calls from this turn
4469
- const responseTexts = [];
4470
- const responseToolCalls = [];
4471
4486
  for (const block of transformed.message.content) {
4472
4487
  if (block.type === 'tool_use') {
4473
4488
  toolCallCount++;
4474
4489
  debugLog$4(`[runner] [native-sdk] 🔧 Tool call: ${block.name}\n`);
4475
- responseToolCalls.push({
4476
- name: block.name ?? 'unknown',
4477
- type: 'function_call',
4478
- arguments: JSON.stringify(block.input).substring(0, 500),
4479
- });
4480
4490
  }
4481
4491
  else if (block.type === 'text' && block.text) {
4482
4492
  textBlockCount++;
4483
- responseTexts.push(block.text);
4484
4493
  }
4485
4494
  }
4486
- // Emit gen_ai.request span as a child of the agent span
4487
- Sentry.withActiveSpan(agentSpan, () => {
4488
- Sentry.startSpan({
4489
- op: 'gen_ai.request',
4490
- name: `request ${modelId}`,
4491
- attributes: {
4492
- 'gen_ai.request.model': modelId,
4493
- 'gen_ai.operation.name': 'request',
4494
- 'gen_ai.request.messages': JSON.stringify(pendingRequestMessages),
4495
- ...(responseTexts.length > 0
4496
- ? { 'gen_ai.response.text': JSON.stringify(responseTexts.map(t => t.substring(0, 500))) }
4497
- : {}),
4498
- ...(responseToolCalls.length > 0
4499
- ? { 'gen_ai.response.tool_calls': JSON.stringify(responseToolCalls) }
4500
- : {}),
4501
- },
4502
- }, () => {
4503
- // Span marks the LLM request/response for this turn
4504
- });
4505
- });
4506
4495
  // Reset pending messages — the next turn's input will be tool results
4507
4496
  pendingRequestMessages = [];
4508
- // Emit gen_ai.execute_tool spans for each tool call in this turn
4509
- for (const block of transformed.message.content) {
4510
- if (block.type === 'tool_use') {
4511
- Sentry.withActiveSpan(agentSpan, () => {
4512
- Sentry.startSpan({
4513
- op: 'gen_ai.execute_tool',
4514
- name: `execute_tool ${block.name}`,
4515
- attributes: {
4516
- 'gen_ai.tool.name': block.name,
4517
- 'gen_ai.tool.call_id': block.id,
4518
- 'gen_ai.tool.input': JSON.stringify(block.input).substring(0, 1000),
4519
- 'gen_ai.request.model': modelId,
4520
- },
4521
- }, () => {
4522
- // Span marks the tool invocation point
4523
- });
4524
- });
4525
- }
4526
- }
4527
4497
  }
4528
4498
  // --- Accumulate tool results for the next gen_ai.request span ---
4529
4499
  // 'user' messages contain tool_result blocks that feed the next LLM turn.
@@ -4550,32 +4520,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4550
4520
  process.stderr.write(`[native-sdk] SDK init — plugins: ${JSON.stringify(loadedPlugins)}\n`);
4551
4521
  process.stderr.write(`[native-sdk] SDK init — tools: ${toolCount} loaded\n`);
4552
4522
  }
4553
- // Set discovered skills on the agent span
4554
- if (agentSpan) {
4555
- agentSpan.setAttribute('gen_ai.agent.skills', discoveredSkills.join(', '));
4556
- agentSpan.setAttribute('gen_ai.agent.skill_count', discoveredSkills.length);
4557
- }
4558
- if (discoveredSkills.length > 0) {
4559
- Sentry.logger.info('SDK initialized with skills', {
4560
- skillCount: String(discoveredSkills.length),
4561
- skills: discoveredSkills.join(', '),
4562
- pluginCount: String(loadedPlugins.length),
4563
- plugins: loadedPlugins.map(p => p.name).join(', '),
4564
- toolCount: String(toolCount),
4565
- model: initMsg.model ?? modelId,
4566
- workingDirectory,
4567
- });
4568
- }
4569
- else {
4570
- Sentry.logger.warn('SDK initialized but no skills discovered', {
4571
- pluginCount: String(loadedPlugins.length),
4572
- plugins: JSON.stringify(loadedPlugins),
4573
- toolCount: String(toolCount),
4574
- model: initMsg.model ?? modelId,
4575
- workingDirectory,
4576
- platformPluginDir: platformPluginDir ?? 'null',
4577
- });
4578
- }
4579
4523
  }
4580
4524
  // Capture tool_use_summary messages — these indicate skill content loading
4581
4525
  if (sdkMessage.type === 'tool_use_summary') {
@@ -4587,29 +4531,6 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4587
4531
  // Capture result messages — record token usage and cost on the agent span
4588
4532
  if (sdkMessage.type === 'result') {
4589
4533
  const resultMsg = sdkMessage;
4590
- if (agentSpan) {
4591
- // Standard gen_ai token usage attributes (Sentry AI Agent Monitoring spec)
4592
- agentSpan.setAttribute('gen_ai.usage.input_tokens', resultMsg.usage?.input_tokens ?? 0);
4593
- agentSpan.setAttribute('gen_ai.usage.output_tokens', resultMsg.usage?.output_tokens ?? 0);
4594
- agentSpan.setAttribute('gen_ai.usage.total_tokens', (resultMsg.usage?.input_tokens ?? 0) + (resultMsg.usage?.output_tokens ?? 0));
4595
- if (resultMsg.usage?.cache_read_input_tokens) {
4596
- agentSpan.setAttribute('gen_ai.usage.input_tokens.cached', resultMsg.usage.cache_read_input_tokens);
4597
- }
4598
- if (resultMsg.usage?.cache_creation_input_tokens) {
4599
- agentSpan.setAttribute('gen_ai.usage.input_tokens.cache_write', resultMsg.usage.cache_creation_input_tokens);
4600
- }
4601
- // Response text (truncated for span safety)
4602
- if (resultMsg.result) {
4603
- agentSpan.setAttribute('gen_ai.response.text', JSON.stringify(resultMsg.result.substring(0, 1000)));
4604
- }
4605
- // Custom (non-spec) attributes for operational insight
4606
- agentSpan.setAttribute('hatchway.cost_usd', resultMsg.total_cost_usd ?? 0);
4607
- agentSpan.setAttribute('hatchway.num_turns', resultMsg.num_turns ?? 0);
4608
- agentSpan.setAttribute('hatchway.num_tool_calls', toolCallCount);
4609
- agentSpan.setAttribute('hatchway.result', resultMsg.subtype ?? 'unknown');
4610
- agentSpan.setAttribute('hatchway.duration_ms', resultMsg.duration_ms ?? 0);
4611
- agentSpan.setAttribute('hatchway.duration_api_ms', resultMsg.duration_api_ms ?? 0);
4612
- }
4613
4534
  if (resultMsg.subtype === 'success') {
4614
4535
  debugLog$4(`[runner] [native-sdk] ✅ Query complete - ${resultMsg.num_turns} turns, $${resultMsg.total_cost_usd?.toFixed(4)} USD\n`);
4615
4536
  }
@@ -4622,16 +4543,8 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4622
4543
  }
4623
4544
  catch (error) {
4624
4545
  debugLog$4(`[runner] [native-sdk] ❌ Error: ${error instanceof Error ? error.message : String(error)}\n`);
4625
- if (agentSpan) {
4626
- agentSpan.setStatus({ code: 2, message: error instanceof Error ? error.message : String(error) });
4627
- }
4628
- Sentry.captureException(error);
4629
4546
  throw error;
4630
4547
  }
4631
- finally {
4632
- // End the agent span regardless of success/failure
4633
- agentSpan?.end();
4634
- }
4635
4548
  };
4636
4549
  }
4637
4550
 
@@ -4840,20 +4753,6 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
4840
4753
  // Parse model ID
4841
4754
  const { provider: providerID, model: modelID } = parseModelId(normalizeModelId(modelId));
4842
4755
  let sessionId = null;
4843
- // Start Sentry AI agent span for the entire OpenCode query
4844
- // This provides visibility into AI operations in Sentry's trace view
4845
- const aiSpan = Sentry.startInactiveSpan({
4846
- name: 'opencode.query',
4847
- op: 'ai.pipeline',
4848
- attributes: {
4849
- 'ai.pipeline.name': 'opencode',
4850
- 'ai.model_id': `${providerID}/${modelID}`,
4851
- 'ai.provider': providerID,
4852
- 'ai.streaming': true,
4853
- 'gen_ai.system': 'opencode',
4854
- 'gen_ai.request.model': modelID,
4855
- },
4856
- });
4857
4756
  try {
4858
4757
  // Step 1: Create a session
4859
4758
  debugLog$3('[runner] [opencode-sdk] Creating session...');
@@ -4871,10 +4770,6 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
4871
4770
  const session = await sessionResponse.json();
4872
4771
  sessionId = session.id;
4873
4772
  debugLog$3(`[runner] [opencode-sdk] Session created: ${sessionId}`);
4874
- // Update span with session info
4875
- if (sessionId) {
4876
- aiSpan?.setAttribute('opencode.session_id', sessionId);
4877
- }
4878
4773
  // Step 2: Subscribe to events
4879
4774
  debugLog$3('[runner] [opencode-sdk] Subscribing to events...');
4880
4775
  const eventResponse = await fetch(`${baseUrl}/event`, {
@@ -5000,16 +4895,9 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
5000
4895
  };
5001
4896
  }
5002
4897
  debugLog$3('[runner] [opencode-sdk] Query complete');
5003
- // Update span with final metrics
5004
- aiSpan?.setAttribute('opencode.tool_calls', toolCallCount);
5005
- aiSpan?.setAttribute('opencode.messages', messageCount);
5006
- aiSpan?.setStatus({ code: 1 }); // OK status
5007
4898
  }
5008
4899
  catch (error) {
5009
4900
  debugLog$3(`[runner] [opencode-sdk] Error: ${error instanceof Error ? error.message : String(error)}`);
5010
- Sentry.captureException(error);
5011
- // Mark span as errored
5012
- aiSpan?.setStatus({ code: 2, message: error instanceof Error ? error.message : String(error) });
5013
4901
  // Yield error result
5014
4902
  yield {
5015
4903
  type: 'result',
@@ -5019,10 +4907,6 @@ function createOpenCodeQuery(modelId = DEFAULT_OPENCODE_MODEL_ID) {
5019
4907
  };
5020
4908
  throw error;
5021
4909
  }
5022
- finally {
5023
- // End the AI span
5024
- aiSpan?.end();
5025
- }
5026
4910
  };
5027
4911
  }
5028
4912
  /**
@@ -5524,15 +5408,6 @@ function createDroidQuery(modelId) {
5524
5408
  process.stderr.write(`[runner] [droid-sdk] Events received: ${messageCount}\n`);
5525
5409
  process.stderr.write(`[runner] [droid-sdk] Tool calls: ${toolCallCount}\n`);
5526
5410
  process.stderr.write(`[runner] [droid-sdk] Last event type: ${lastEventType}\n\n`);
5527
- Sentry.captureException(error, {
5528
- extra: {
5529
- modelId,
5530
- workingDirectory,
5531
- promptLength: prompt.length,
5532
- eventsReceived: messageCount,
5533
- toolCalls: toolCallCount,
5534
- }
5535
- });
5536
5411
  throw error;
5537
5412
  }
5538
5413
  finally {
@@ -5542,6 +5417,39 @@ function createDroidQuery(modelId) {
5542
5417
  };
5543
5418
  }
5544
5419
 
5420
+ // src/lib/path-safety.ts
5421
+ var SLUG_PATTERN = /^[a-zA-Z0-9._-]+$/;
5422
+ var MAX_SLUG_LENGTH = 128;
5423
+ function isValidProjectSlug(slug) {
5424
+ return typeof slug === "string" && slug.length > 0 && slug.length <= MAX_SLUG_LENGTH && SLUG_PATTERN.test(slug) && !slug.includes("..");
5425
+ }
5426
+ function assertValidProjectSlug(slug) {
5427
+ if (!isValidProjectSlug(slug)) {
5428
+ throw new Error(`Invalid project slug: ${String(slug)}`);
5429
+ }
5430
+ }
5431
+ function resolveProjectPath(workspaceRoot, slug) {
5432
+ assertValidProjectSlug(slug);
5433
+ const root = resolve$1(workspaceRoot);
5434
+ const projectPath = resolve$1(root, slug);
5435
+ const rel = relative$1(root, projectPath);
5436
+ if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) {
5437
+ throw new Error(`Invalid project slug: ${slug}`);
5438
+ }
5439
+ return projectPath;
5440
+ }
5441
+ function resolveWithinProject(projectPath, relPath) {
5442
+ if (typeof relPath !== "string" || relPath.length === 0 || relPath.includes("\0")) {
5443
+ throw new Error(`Invalid file path: ${String(relPath)}`);
5444
+ }
5445
+ const fullPath = resolve$1(projectPath, relPath);
5446
+ const rel = relative$1(projectPath, fullPath);
5447
+ if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) {
5448
+ throw new Error("Invalid file path - outside project directory");
5449
+ }
5450
+ return fullPath;
5451
+ }
5452
+
5545
5453
  // src/lib/logging/build-logger.ts
5546
5454
  var BuildLogger = class {
5547
5455
  buildId = null;
@@ -5593,23 +5501,6 @@ var BuildLogger = class {
5593
5501
  } else {
5594
5502
  logFn(`${icon} ${prefix} ${message}`);
5595
5503
  }
5596
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
5597
- try {
5598
- if (level === "warn" || level === "error") {
5599
- Sentry.addBreadcrumb({
5600
- category: `build-logger.${context}`,
5601
- message,
5602
- level: level === "error" ? "error" : "warning",
5603
- data: {
5604
- ...data,
5605
- buildId: this.buildId,
5606
- projectId: this.projectId
5607
- }
5608
- });
5609
- }
5610
- } catch {
5611
- }
5612
- }
5613
5504
  }
5614
5505
  /**
5615
5506
  * Orchestrator-specific logging methods
@@ -5986,12 +5877,12 @@ var init_tags = __esm({
5986
5877
  sdk: "agent"
5987
5878
  },
5988
5879
  {
5989
- value: "claude-opus-4-6",
5990
- label: "Claude Opus 4.6",
5880
+ value: "claude-opus-4-8",
5881
+ label: "Claude Opus 4.8",
5991
5882
  description: "Anthropic Claude - Most capable for complex tasks",
5992
5883
  logo: "/claude.png",
5993
5884
  provider: "claude-code",
5994
- model: "claude-opus-4-6",
5885
+ model: "claude-opus-4-8",
5995
5886
  sdk: "agent"
5996
5887
  },
5997
5888
  {
@@ -6015,10 +5906,10 @@ var init_tags = __esm({
6015
5906
  {
6016
5907
  value: "factory-droid-opus",
6017
5908
  label: "Factory Droid (Opus)",
6018
- description: "Factory Droid SDK with Claude Opus 4.6",
5909
+ description: "Factory Droid SDK with Claude Opus 4.8",
6019
5910
  logo: "/factory.svg",
6020
5911
  provider: "factory-droid",
6021
- model: "claude-opus-4-6-20251101",
5912
+ model: "claude-opus-4-8",
6022
5913
  sdk: "droid"
6023
5914
  },
6024
5915
  {
@@ -7257,12 +7148,6 @@ async function createBuildStream(options) {
7257
7148
  debugLog$1();
7258
7149
  const generator = query(fullPrompt, actualWorkingDir, systemPrompt, agent, options.codexThreadId, messageParts);
7259
7150
  debugLog$1();
7260
- // Capture the active Sentry span BEFORE creating the ReadableStream.
7261
- // The ReadableStream.start() callback runs in a new async context where the
7262
- // parent build.runner span is no longer active. We restore it with withActiveSpan()
7263
- // so that gen_ai.invoke_agent spans created inside the query generator are
7264
- // properly nested as children of the build.runner span.
7265
- const parentSpan = Sentry.getActiveSpan();
7266
7151
  // Create a ReadableStream from the AsyncGenerator
7267
7152
  const stream = new ReadableStream({
7268
7153
  async start(controller) {
@@ -7298,13 +7183,7 @@ async function createBuildStream(options) {
7298
7183
  process.chdir(originalCwd);
7299
7184
  }
7300
7185
  };
7301
- // Restore the parent span context so child spans nest correctly
7302
- if (parentSpan) {
7303
- await Sentry.withActiveSpan(parentSpan, consume);
7304
- }
7305
- else {
7306
- await consume();
7307
- }
7186
+ await consume();
7308
7187
  },
7309
7188
  });
7310
7189
  debugLog$1();
@@ -7935,6 +7814,12 @@ async function runHealthCheck(projectId, port) {
7935
7814
  };
7936
7815
  }
7937
7816
  }
7817
+ /**
7818
+ * Get active process for a project
7819
+ */
7820
+ function getDevServer(projectId) {
7821
+ return activeProcesses.get(projectId);
7822
+ }
7938
7823
  /**
7939
7824
  * Get all active project IDs
7940
7825
  */
@@ -7960,6 +7845,7 @@ var processManager = /*#__PURE__*/Object.freeze({
7960
7845
  checkPortInUse: checkPortInUse,
7961
7846
  findAvailablePort: findAvailablePort,
7962
7847
  getAllActiveProjectIds: getAllActiveProjectIds,
7848
+ getDevServer: getDevServer,
7963
7849
  runHealthCheck: runHealthCheck,
7964
7850
  setSilentMode: setSilentMode$1,
7965
7851
  startDevServer: startDevServer,
@@ -9876,12 +9762,12 @@ var TAG_DEFINITIONS = [
9876
9762
  sdk: "agent"
9877
9763
  },
9878
9764
  {
9879
- value: "claude-opus-4-6",
9880
- label: "Claude Opus 4.6",
9765
+ value: "claude-opus-4-8",
9766
+ label: "Claude Opus 4.8",
9881
9767
  description: "Anthropic Claude - Most capable for complex tasks",
9882
9768
  logo: "/claude.png",
9883
9769
  provider: "claude-code",
9884
- model: "claude-opus-4-6",
9770
+ model: "claude-opus-4-8",
9885
9771
  sdk: "agent"
9886
9772
  },
9887
9773
  {
@@ -9905,10 +9791,10 @@ var TAG_DEFINITIONS = [
9905
9791
  {
9906
9792
  value: "factory-droid-opus",
9907
9793
  label: "Factory Droid (Opus)",
9908
- description: "Factory Droid SDK with Claude Opus 4.6",
9794
+ description: "Factory Droid SDK with Claude Opus 4.8",
9909
9795
  logo: "/factory.svg",
9910
9796
  provider: "factory-droid",
9911
- model: "claude-opus-4-6-20251101",
9797
+ model: "claude-opus-4-8",
9912
9798
  sdk: "droid"
9913
9799
  },
9914
9800
  {
@@ -10127,7 +10013,7 @@ var TAG_DEFINITIONS = [
10127
10013
  const MODEL_MAP = {
10128
10014
  'claude-haiku-4-5': 'claude-sonnet-4-6', // Haiku 4.5 not yet available, use Sonnet
10129
10015
  'claude-sonnet-4-6': 'claude-sonnet-4-6',
10130
- 'claude-opus-4-6': 'claude-opus-4-6',
10016
+ 'claude-opus-4-8': 'claude-opus-4-8',
10131
10017
  };
10132
10018
  function resolveModelName(modelId) {
10133
10019
  return MODEL_MAP[modelId] || 'claude-sonnet-4-6';
@@ -10195,7 +10081,6 @@ CRITICAL: Your response must START with { and END with }. Output only the JSON o
10195
10081
  }
10196
10082
  catch (error) {
10197
10083
  console.error('[project-analyzer] SDK query failed:', error);
10198
- Sentry.captureException(error);
10199
10084
  throw error;
10200
10085
  }
10201
10086
  if (!responseText) {
@@ -10731,8 +10616,6 @@ class HmrProxyManager {
10731
10616
  // Export singleton instance
10732
10617
  const hmrProxyManager = new HmrProxyManager();
10733
10618
 
10734
- // Sentry is initialized via --import flag (see package.json scripts)
10735
- // This ensures instrumentation loads before any other modules
10736
10619
  // AI SDK log warnings disabled (legacy - no longer using AI SDK)
10737
10620
  /**
10738
10621
  * Truncate strings for logging to prevent excessive output
@@ -10898,13 +10781,6 @@ function createCodexQuery() {
10898
10781
  log(`🚀 [codex-query] Turn ${turnCount}: ${turnCount === 1 ? 'Initial request' : 'Continuing work'}...`);
10899
10782
  // Log full prompt being sent to Codex
10900
10783
  if (turnCount === 1) {
10901
- Sentry.logger.info(Sentry.logger.fmt `Full Codex prompt (Turn 1) ${{
10902
- prompt: turnPrompt,
10903
- promptLength: turnPrompt.length,
10904
- promptPreview: turnPrompt.substring(0, 200),
10905
- operation: 'codex_query',
10906
- turnCount: 1,
10907
- }}`);
10908
10784
  // Also log to file
10909
10785
  fileLog.info('━━━ FULL CODEX PROMPT ━━━');
10910
10786
  fileLog.info(turnPrompt);
@@ -11297,29 +11173,11 @@ async function startRunner(options = {}) {
11297
11173
  throw lastError || new Error('Persistence failed after retries');
11298
11174
  }
11299
11175
  async function persistBuildEventDirect(context, event) {
11300
- return Sentry.startSpan({
11301
- name: `persist.build-event.${event.type}`,
11302
- op: 'http.client',
11303
- attributes: {
11304
- 'http.method': 'POST',
11305
- 'http.url': `${apiBaseUrl}/api/build-events`,
11306
- 'event.type': event.type,
11307
- 'event.tool_name': event.toolName || undefined,
11308
- },
11309
- }, async () => {
11310
- // Get trace context for propagation
11311
- const traceData = Sentry.getTraceData();
11176
+ return await (async () => {
11312
11177
  const headers = {
11313
11178
  'Authorization': `Bearer ${runnerSharedSecret}`,
11314
11179
  'Content-Type': 'application/json',
11315
11180
  };
11316
- // Add Sentry trace headers for distributed tracing
11317
- if (traceData['sentry-trace']) {
11318
- headers['sentry-trace'] = traceData['sentry-trace'];
11319
- }
11320
- if (traceData.baggage) {
11321
- headers['baggage'] = traceData.baggage;
11322
- }
11323
11181
  const response = await fetchWithRetry(`${apiBaseUrl}/api/build-events`, {
11324
11182
  method: 'POST',
11325
11183
  headers,
@@ -11337,7 +11195,7 @@ async function startRunner(options = {}) {
11337
11195
  if (!response.ok) {
11338
11196
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
11339
11197
  }
11340
- });
11198
+ })();
11341
11199
  }
11342
11200
  /**
11343
11201
  * Send a build event to the HTTP persistence endpoint.
@@ -11415,16 +11273,7 @@ async function startRunner(options = {}) {
11415
11273
  * Send a runner lifecycle event to the HTTP persistence endpoint.
11416
11274
  */
11417
11275
  async function persistRunnerEvent(event) {
11418
- // Wrap in Sentry span to create proper trace hierarchy
11419
- return Sentry.startSpan({
11420
- name: `persist.runner-event.${event.type}`,
11421
- op: 'http.client',
11422
- attributes: {
11423
- 'http.method': 'POST',
11424
- 'http.url': `${apiBaseUrl}/api/runner/events`,
11425
- 'event.type': event.type,
11426
- },
11427
- }, async () => {
11276
+ return await (async () => {
11428
11277
  try {
11429
11278
  const response = await fetch(`${apiBaseUrl}/api/runner/events`, {
11430
11279
  method: 'POST',
@@ -11440,9 +11289,9 @@ async function startRunner(options = {}) {
11440
11289
  }
11441
11290
  catch (error) {
11442
11291
  console.error('[runner] Error persisting runner event:', error);
11443
- throw error; // Re-throw so Sentry span records the error
11292
+ throw error;
11444
11293
  }
11445
- });
11294
+ })();
11446
11295
  }
11447
11296
  // Event types that trigger DB writes and should be persisted via HTTP
11448
11297
  const DB_WORTHY_RUNNER_EVENTS = [
@@ -11468,16 +11317,6 @@ async function startRunner(options = {}) {
11468
11317
  }
11469
11318
  const sendOperation = () => {
11470
11319
  try {
11471
- // Attach trace context to ALL events for distributed tracing
11472
- // This allows events to be linked back to the originating frontend request
11473
- const span = Sentry.getActiveSpan();
11474
- if (span) {
11475
- const traceData = Sentry.getTraceData();
11476
- event._sentry = {
11477
- trace: traceData['sentry-trace'],
11478
- baggage: traceData.baggage,
11479
- };
11480
- }
11481
11320
  const eventJson = JSON.stringify(event);
11482
11321
  // Only log important events
11483
11322
  if (event.type === "error") {
@@ -11963,7 +11802,6 @@ async function startRunner(options = {}) {
11963
11802
  break;
11964
11803
  }
11965
11804
  case "start-tunnel": {
11966
- const tunnelStartTime = Date.now();
11967
11805
  try {
11968
11806
  const { port } = command.payload;
11969
11807
  log(`🔗 Starting tunnel for port ${port}...`);
@@ -11978,15 +11816,6 @@ async function startRunner(options = {}) {
11978
11816
  // Create tunnel
11979
11817
  const tunnelUrl = await tunnelManager.createTunnel(port);
11980
11818
  logger.tunnel({ port, url: tunnelUrl, status: 'created' });
11981
- // Instrument tunnel startup timing
11982
- const tunnelDuration = Date.now() - tunnelStartTime;
11983
- Sentry.metrics.distribution('tunnel_startup_duration', tunnelDuration, {
11984
- unit: 'millisecond',
11985
- attributes: {
11986
- port: port.toString(),
11987
- success: 'true'
11988
- }
11989
- });
11990
11819
  sendEvent({
11991
11820
  type: "tunnel-created",
11992
11821
  ...buildEventBase(command.projectId, command.id),
@@ -11996,16 +11825,6 @@ async function startRunner(options = {}) {
11996
11825
  }
11997
11826
  catch (error) {
11998
11827
  console.error("Failed to create tunnel:", error);
11999
- // Instrument failed tunnel startup timing
12000
- const tunnelDuration = Date.now() - tunnelStartTime;
12001
- Sentry.metrics.distribution('tunnel_startup_duration', tunnelDuration, {
12002
- unit: 'millisecond',
12003
- attributes: {
12004
- port: command.payload.port.toString(),
12005
- success: 'false',
12006
- error_type: error instanceof Error ? error.constructor.name : 'unknown'
12007
- }
12008
- });
12009
11828
  sendEvent({
12010
11829
  type: "error",
12011
11830
  ...buildEventBase(command.projectId, command.id),
@@ -12054,7 +11873,8 @@ async function startRunner(options = {}) {
12054
11873
  case "delete-project-files": {
12055
11874
  try {
12056
11875
  const { slug } = command.payload;
12057
- const projectPath = join(WORKSPACE_ROOT, slug);
11876
+ // Validates the slug and guarantees the path stays inside the workspace
11877
+ const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
12058
11878
  console.log(`[runner] 🗑️ Deleting project files for slug: ${slug}`);
12059
11879
  console.log(`[runner] Path: ${projectPath}`);
12060
11880
  // First, stop any running dev server for this project to release file locks
@@ -12126,13 +11946,9 @@ async function startRunner(options = {}) {
12126
11946
  case "read-file": {
12127
11947
  try {
12128
11948
  const { slug, filePath } = command.payload;
12129
- const projectPath = join(WORKSPACE_ROOT, slug);
12130
- const fullPath = join(projectPath, filePath);
11949
+ const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
11950
+ const fullPath = resolveWithinProject(projectPath, filePath);
12131
11951
  console.log(`[runner] 📖 Reading file: ${filePath} from project: ${slug}`);
12132
- // Security: Ensure path is within project directory
12133
- if (!fullPath.startsWith(projectPath)) {
12134
- throw new Error("Invalid file path - outside project directory");
12135
- }
12136
11952
  const { readFile, stat } = await import('node:fs/promises');
12137
11953
  const stats = await stat(fullPath);
12138
11954
  const content = await readFile(fullPath, "utf-8");
@@ -12160,13 +11976,9 @@ async function startRunner(options = {}) {
12160
11976
  case "write-file": {
12161
11977
  try {
12162
11978
  const { slug, filePath, content } = command.payload;
12163
- const projectPath = join(WORKSPACE_ROOT, slug);
12164
- const fullPath = join(projectPath, filePath);
11979
+ const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
11980
+ const fullPath = resolveWithinProject(projectPath, filePath);
12165
11981
  console.log(`[runner] 💾 Writing file: ${filePath} to project: ${slug}`);
12166
- // Security: Ensure path is within project directory
12167
- if (!fullPath.startsWith(projectPath)) {
12168
- throw new Error("Invalid file path - outside project directory");
12169
- }
12170
11982
  const { writeFile } = await import('node:fs/promises');
12171
11983
  await writeFile(fullPath, content, "utf-8");
12172
11984
  console.log(`[runner] ✅ File written successfully (${content.length} bytes)`);
@@ -12191,14 +12003,12 @@ async function startRunner(options = {}) {
12191
12003
  case "list-files": {
12192
12004
  try {
12193
12005
  const { slug, path: subPath } = command.payload;
12194
- const projectPath = join(WORKSPACE_ROOT, slug);
12195
- const targetPath = subPath ? join(projectPath, subPath) : projectPath;
12006
+ const projectPath = resolveProjectPath(WORKSPACE_ROOT, slug);
12007
+ const targetPath = subPath
12008
+ ? resolveWithinProject(projectPath, subPath)
12009
+ : projectPath;
12196
12010
  console.log(`[runner] 📁 Listing files for project: ${slug}`);
12197
12011
  console.log(`[runner] Path: ${targetPath}`);
12198
- // Security: Ensure path is within project directory
12199
- if (!targetPath.startsWith(projectPath)) {
12200
- throw new Error("Invalid path - outside project directory");
12201
- }
12202
12012
  const { readdir, stat } = await import('node:fs/promises');
12203
12013
  const entries = await readdir(targetPath);
12204
12014
  const files = await Promise.all(entries.map(async (name) => {
@@ -12241,7 +12051,7 @@ async function startRunner(options = {}) {
12241
12051
  const model = agent === 'claude-code' &&
12242
12052
  (claudeModelFromPayload === 'claude-haiku-4-5' ||
12243
12053
  claudeModelFromPayload === 'claude-sonnet-4-6' ||
12244
- claudeModelFromPayload === 'claude-opus-4-6')
12054
+ claudeModelFromPayload === 'claude-opus-4-8')
12245
12055
  ? claudeModelFromPayload
12246
12056
  : DEFAULT_CLAUDE_MODEL_ID;
12247
12057
  logger.buildReceived({
@@ -12262,7 +12072,6 @@ async function startRunner(options = {}) {
12262
12072
  fileLog.info("Agent:", command.payload?.agent);
12263
12073
  fileLog.info("Template:", command.payload?.template);
12264
12074
  // REMOVED: Manual Sentry span creation - rely on automatic instrumentation
12265
- // await Sentry.startSpan({ name: "runner.build", op: "ai.build", ... }, async () => {
12266
12075
  // Build operation (previously wrapped in Sentry span)
12267
12076
  try {
12268
12077
  loggedFirstChunk = false;
@@ -12270,9 +12079,11 @@ async function startRunner(options = {}) {
12270
12079
  throw new Error("Invalid build payload");
12271
12080
  }
12272
12081
  // Calculate the project directory using slug
12082
+ // Slug may be LLM-derived from the user prompt - validate before
12083
+ // it touches the filesystem
12273
12084
  const projectSlug = command.payload.projectSlug || command.projectId;
12274
12085
  const projectName = command.payload.projectName || projectSlug;
12275
- const projectDirectory = resolve(WORKSPACE_ROOT, projectSlug);
12086
+ const projectDirectory = resolveProjectPath(WORKSPACE_ROOT, projectSlug);
12276
12087
  log("project directory:", projectDirectory);
12277
12088
  log("project slug:", projectSlug);
12278
12089
  log("project name:", projectName);
@@ -12283,7 +12094,7 @@ async function startRunner(options = {}) {
12283
12094
  const claudeModel = agent === "claude-code" &&
12284
12095
  (command.payload.claudeModel === "claude-haiku-4-5" ||
12285
12096
  command.payload.claudeModel === "claude-sonnet-4-6" ||
12286
- command.payload.claudeModel === "claude-opus-4-6")
12097
+ command.payload.claudeModel === "claude-opus-4-8")
12287
12098
  ? command.payload.claudeModel
12288
12099
  : DEFAULT_CLAUDE_MODEL_ID;
12289
12100
  // For factory-droid, use the droidModel from payload
@@ -12708,7 +12519,7 @@ async function startRunner(options = {}) {
12708
12519
  // Detect framework from generated files
12709
12520
  let detectedFramework = null;
12710
12521
  try {
12711
- const { detectFrameworkFromFilesystem } = await import('./chunks/port-allocator-BENntRMG.js');
12522
+ const { detectFrameworkFromFilesystem } = await import('./chunks/port-allocator-DAjm7X-F.js');
12712
12523
  const framework = await detectFrameworkFromFilesystem(projectDirectory);
12713
12524
  detectedFramework = framework;
12714
12525
  if (framework) {
@@ -12810,10 +12621,6 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
12810
12621
  catch (error) {
12811
12622
  const errorMessage = error instanceof Error ? error.message : "Failed to run build";
12812
12623
  logger.buildFailed(errorMessage);
12813
- Sentry.getActiveSpan()?.setStatus({
12814
- code: 2, // SPAN_STATUS_ERROR
12815
- message: "Build failed",
12816
- });
12817
12624
  sendEvent({
12818
12625
  type: "build-failed",
12819
12626
  ...buildEventBase(command.projectId, command.id),
@@ -12836,9 +12643,22 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
12836
12643
  // HTTP proxy request - fetch from local dev server and return response
12837
12644
  try {
12838
12645
  const { requestId, method, path: reqPath, headers, body, port } = command.payload;
12839
- debugLog(`🔀 HTTP proxy request: ${method} ${reqPath} localhost:${port}`);
12646
+ // SSRF guard: only proxy to the dev server this runner started for
12647
+ // this project - never to arbitrary localhost ports
12648
+ const devServer = getDevServer(command.projectId);
12649
+ if (!devServer?.port) {
12650
+ throw new Error(`No active dev server for project ${command.projectId}`);
12651
+ }
12652
+ if (port !== undefined && port !== devServer.port) {
12653
+ throw new Error(`Port ${port} does not match project dev server port ${devServer.port}`);
12654
+ }
12655
+ const targetPort = devServer.port;
12656
+ if (typeof reqPath !== 'string' || !reqPath.startsWith('/')) {
12657
+ throw new Error(`Invalid proxy path: ${String(reqPath)}`);
12658
+ }
12659
+ debugLog(`🔀 HTTP proxy request: ${method} ${reqPath} → localhost:${targetPort}`);
12840
12660
  // Build the target URL
12841
- const targetUrl = `http://localhost:${port}${reqPath}`;
12661
+ const targetUrl = `http://localhost:${targetPort}${reqPath}`;
12842
12662
  // Decode body if present
12843
12663
  const requestBody = body ? Buffer.from(body, 'base64') : undefined;
12844
12664
  // Fetch from dev server
@@ -12847,7 +12667,7 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
12847
12667
  headers: {
12848
12668
  ...headers,
12849
12669
  // Remove hop-by-hop headers
12850
- 'host': `localhost:${port}`,
12670
+ 'host': `localhost:${targetPort}`,
12851
12671
  },
12852
12672
  body: requestBody,
12853
12673
  // Don't follow redirects - let the client handle them
@@ -12917,8 +12737,33 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
12917
12737
  }
12918
12738
  case "hmr-connect": {
12919
12739
  // Connect to local HMR WebSocket server
12740
+ // SSRF guard: only connect to the dev server this runner started for
12741
+ // this project - never to an arbitrary server-supplied port
12920
12742
  const { connectionId, port, protocol } = command.payload;
12921
- hmrProxyManager.connect(connectionId, port, command.projectId, protocol);
12743
+ const hmrDevServer = getDevServer(command.projectId);
12744
+ // Surface a hmr-error (not a silent break) so the frontend's pending
12745
+ // connection fails fast instead of hanging until its timeout.
12746
+ if (!hmrDevServer?.port) {
12747
+ console.warn(`[runner] ⚠️ hmr-connect rejected: no active dev server for project ${command.projectId}`);
12748
+ sendEvent({
12749
+ type: "hmr-error",
12750
+ ...buildEventBase(command.projectId, command.id),
12751
+ connectionId,
12752
+ error: "No active dev server for project",
12753
+ });
12754
+ break;
12755
+ }
12756
+ if (port !== undefined && port !== hmrDevServer.port) {
12757
+ console.warn(`[runner] ⚠️ hmr-connect rejected: port ${port} does not match dev server port ${hmrDevServer.port}`);
12758
+ sendEvent({
12759
+ type: "hmr-error",
12760
+ ...buildEventBase(command.projectId, command.id),
12761
+ connectionId,
12762
+ error: "Requested port does not match the project's dev server",
12763
+ });
12764
+ break;
12765
+ }
12766
+ hmrProxyManager.connect(connectionId, hmrDevServer.port, command.projectId, protocol);
12922
12767
  break;
12923
12768
  }
12924
12769
  case "hmr-message": {
@@ -13115,15 +12960,6 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
13115
12960
  lastCommandReceived = Date.now(); // Reset command timer on new connection
13116
12961
  // Update logger connection status
13117
12962
  logger.setConnected(true);
13118
- Sentry.logger.info('Runner connected to server', {
13119
- runnerId: RUNNER_ID,
13120
- version: getPackageVersion(),
13121
- user: os$1.userInfo().username,
13122
- hostname: os$1.hostname(),
13123
- platform: os$1.platform(),
13124
- serverUrl: WS_URL,
13125
- workspace: WORKSPACE_ROOT,
13126
- });
13127
12963
  debugLog("Health check: ping/pong enabled, command timeout: 5 minutes");
13128
12964
  publishStatus();
13129
12965
  scheduleHeartbeat();
@@ -13164,107 +13000,10 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
13164
13000
  const command = JSON.parse(String(data));
13165
13001
  // BUG FIX: Update lastCommandReceived timestamp
13166
13002
  lastCommandReceived = Date.now();
13167
- // Get projectId - analyze-project commands don't have one yet
13168
- const projectIdForTelemetry = command.type === 'analyze-project'
13169
- ? 'pending-analysis'
13170
- : command.projectId;
13171
- // Continue trace from frontend - each build now starts its trace in the frontend
13172
- // This creates a span within the continued trace for the runner's work
13173
- if (command._sentry?.trace) {
13174
- console.log("[runner] Continuing trace from frontend:", command._sentry.trace.substring(0, 50));
13175
- await Sentry.continueTrace({
13176
- sentryTrace: command._sentry.trace,
13177
- baggage: command._sentry.baggage,
13178
- }, async () => {
13179
- // Create a span for this command execution within the continued trace
13180
- await Sentry.startSpan({
13181
- name: `runner.${command.type}`,
13182
- op: command.type === 'start-build' ? 'build.runner' : `runner.${command.type}`,
13183
- attributes: {
13184
- 'command.type': command.type,
13185
- 'command.id': command.id,
13186
- 'project.id': projectIdForTelemetry,
13187
- 'trace.continued': true,
13188
- },
13189
- }, async (span) => {
13190
- try {
13191
- Sentry.setTag("command_type", command.type);
13192
- Sentry.setTag("project_id", projectIdForTelemetry);
13193
- Sentry.setTag("command_id", command.id);
13194
- // Capture build metrics for start-build commands
13195
- if (command.type === 'start-build' && command.payload) {
13196
- const agent = command.payload.agent ?? 'claude-code';
13197
- const claudeModel = agent === 'claude-code' &&
13198
- (command.payload.claudeModel === 'claude-haiku-4-5' ||
13199
- command.payload.claudeModel === 'claude-sonnet-4-6' ||
13200
- command.payload.claudeModel === 'claude-opus-4-6')
13201
- ? command.payload.claudeModel
13202
- : 'claude-sonnet-4-6';
13203
- Sentry.metrics.count('runner.build.started', 1, {
13204
- attributes: {
13205
- project_id: command.projectId,
13206
- model: agent === 'claude-code' ? claudeModel : agent,
13207
- framework: command.payload.template?.framework || 'unknown',
13208
- operation_type: command.payload.operationType || 'initial-build',
13209
- }
13210
- });
13211
- }
13212
- await handleCommand(command);
13213
- }
13214
- catch (error) {
13215
- span.setStatus({ code: 2, message: error instanceof Error ? error.message : 'Command failed' });
13216
- throw error;
13217
- }
13218
- });
13219
- });
13220
- }
13221
- else {
13222
- console.log("[runner] No trace context - starting isolated span");
13223
- // Create an isolated span when no trace context is provided
13224
- await Sentry.startSpan({
13225
- name: `runner.${command.type}`,
13226
- op: command.type === 'start-build' ? 'build.runner' : `runner.${command.type}`,
13227
- attributes: {
13228
- 'command.type': command.type,
13229
- 'command.id': command.id,
13230
- 'project.id': projectIdForTelemetry,
13231
- 'trace.continued': false,
13232
- },
13233
- }, async (span) => {
13234
- try {
13235
- Sentry.setTag("command_type", command.type);
13236
- Sentry.setTag("project_id", projectIdForTelemetry);
13237
- Sentry.setTag("command_id", command.id);
13238
- // Capture build metrics for start-build commands
13239
- if (command.type === 'start-build' && command.payload) {
13240
- const agent = command.payload.agent ?? 'claude-code';
13241
- const claudeModel = agent === 'claude-code' &&
13242
- (command.payload.claudeModel === 'claude-haiku-4-5' ||
13243
- command.payload.claudeModel === 'claude-sonnet-4-6' ||
13244
- command.payload.claudeModel === 'claude-opus-4-6')
13245
- ? command.payload.claudeModel
13246
- : 'claude-sonnet-4-6';
13247
- Sentry.metrics.count('runner.build.started', 1, {
13248
- attributes: {
13249
- project_id: command.projectId,
13250
- model: agent === 'claude-code' ? claudeModel : agent,
13251
- framework: command.payload.template?.framework || 'unknown',
13252
- operation_type: command.payload.operationType || 'initial-build',
13253
- }
13254
- });
13255
- }
13256
- await handleCommand(command);
13257
- }
13258
- catch (error) {
13259
- span.setStatus({ code: 2, message: error instanceof Error ? error.message : 'Command failed' });
13260
- throw error;
13261
- }
13262
- });
13263
- }
13003
+ await handleCommand(command);
13264
13004
  }
13265
13005
  catch (error) {
13266
13006
  console.error("Failed to parse command", error);
13267
- Sentry.captureException(error);
13268
13007
  sendEvent({
13269
13008
  type: "error",
13270
13009
  ...buildEventBase(undefined, randomUUID$1()),
@@ -13355,8 +13094,6 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
13355
13094
  await stopAllDevServers(tunnelManager);
13356
13095
  // Final cleanup of any remaining tunnels
13357
13096
  await tunnelManager.closeAll();
13358
- // Flush Sentry events before exiting
13359
- await Sentry.flush(2000);
13360
13097
  log("shutdown complete");
13361
13098
  };
13362
13099
  process.on("SIGINT", async () => {