webmux 0.31.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6959,9 +6959,67 @@ var require_public_api = __commonJS((exports) => {
6959
6959
 
6960
6960
  // backend/src/server.ts
6961
6961
  import { randomUUID as randomUUID3 } from "crypto";
6962
- import { join as join8, resolve as resolve9 } from "path";
6962
+ import { join as join7, resolve as resolve9 } from "path";
6963
6963
  import { mkdirSync } from "fs";
6964
6964
  import { networkInterfaces } from "os";
6965
+ // package.json
6966
+ var package_default = {
6967
+ name: "webmux",
6968
+ version: "0.32.0",
6969
+ description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
6970
+ type: "module",
6971
+ repository: {
6972
+ type: "git",
6973
+ url: "git+https://github.com/windmill-labs/workmux-web.git"
6974
+ },
6975
+ homepage: "https://github.com/windmill-labs/workmux-web",
6976
+ keywords: [
6977
+ "workmux",
6978
+ "git-worktree",
6979
+ "tmux",
6980
+ "dashboard",
6981
+ "terminal",
6982
+ "ai-agent"
6983
+ ],
6984
+ bin: {
6985
+ webmux: "bin/webmux.js"
6986
+ },
6987
+ workspaces: [
6988
+ "backend",
6989
+ "frontend",
6990
+ "packages/*"
6991
+ ],
6992
+ dependencies: {},
6993
+ scripts: {
6994
+ dev: "bash dev.sh",
6995
+ start: "bun bin/webmux.js",
6996
+ build: "cd frontend && bun run build && cd .. && bun build backend/src/server.ts --target=bun --outfile=backend/dist/server.js && bun build bin/src/webmux.ts --target=bun --outfile=bin/webmux.js",
6997
+ prepublishOnly: "bun run build",
6998
+ test: "bun run --cwd backend test && bun test packages/api-contract/src && bun test bin/src && bun run --cwd frontend test",
6999
+ "test:coverage": "bun run --cwd backend test --coverage && bun test --coverage packages/api-contract/src && bun test --coverage bin/src && bun run --cwd frontend test:coverage"
7000
+ },
7001
+ files: [
7002
+ "bin/webmux.js",
7003
+ "backend/dist/",
7004
+ "frontend/dist/"
7005
+ ],
7006
+ devDependencies: {
7007
+ "@webmux/api-contract": "workspace:*",
7008
+ "@clack/prompts": "^1.1.0",
7009
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
7010
+ "@tailwindcss/vite": "^4.2.0",
7011
+ "@types/bun": "latest",
7012
+ "@xterm/addon-fit": "^0.10.0",
7013
+ "@xterm/addon-web-links": "^0.11.0",
7014
+ "@xterm/xterm": "^5.5.0",
7015
+ svelte: "^5.0.0",
7016
+ "svelte-check": "^4.0.0",
7017
+ tailwindcss: "^4.2.0",
7018
+ typescript: "^5.0.0",
7019
+ vite: "^6.0.0"
7020
+ },
7021
+ license: "MIT"
7022
+ };
6965
7023
 
6966
7024
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
6967
7025
  var exports_external = {};
@@ -10936,7 +10994,7 @@ var coerce = {
10936
10994
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
10937
10995
  };
10938
10996
  var NEVER = INVALID;
10939
- // node_modules/.bun/@ts-rest+core@3.52.1+1c8a9bbc689bc595/node_modules/@ts-rest/core/index.esm.mjs
10997
+ // node_modules/.bun/@ts-rest+core@3.52.1+596964f7fee2c930/node_modules/@ts-rest/core/index.esm.mjs
10940
10998
  var isZodObjectStrict = (obj) => {
10941
10999
  return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
10942
11000
  };
@@ -11028,6 +11086,30 @@ var EnabledResponseSchema = exports_external.object({
11028
11086
  var BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
11029
11087
  var AgentIdSchema = exports_external.string().trim().min(1);
11030
11088
  var WorktreeCreateModeSchema = exports_external.enum(["new", "existing"]);
11089
+ var LinearIssueIdSchema = exports_external.string().regex(/^[A-Z]+-\d+$/, "Expected Linear issue id (e.g. ENG-123)");
11090
+ var LinearTeamKeySchema = exports_external.string().regex(/^[A-Z]+$/, "Expected Linear team key (e.g. ENG)");
11091
+ var PostWorktreeToLinearTargetSchema = exports_external.discriminatedUnion("kind", [
11092
+ exports_external.object({ kind: exports_external.literal("issue"), issueId: LinearIssueIdSchema }),
11093
+ exports_external.object({ kind: exports_external.literal("team"), teamKey: LinearTeamKeySchema, title: exports_external.string().trim().min(1).optional() })
11094
+ ]);
11095
+ var PostWorktreeToLinearRequestSchema = exports_external.object({
11096
+ target: PostWorktreeToLinearTargetSchema
11097
+ });
11098
+ var PostWorktreeToLinearResponseSchema = exports_external.object({
11099
+ ok: exports_external.literal(true),
11100
+ issueId: exports_external.string(),
11101
+ issueUrl: exports_external.string(),
11102
+ commentUrl: exports_external.string().nullable(),
11103
+ attachmentUrl: exports_external.string()
11104
+ });
11105
+ var FromLinearInputSchema = exports_external.object({
11106
+ issueId: LinearIssueIdSchema,
11107
+ conversationContext: exports_external.string().optional()
11108
+ });
11109
+ var OneshotConfigSchema = exports_external.object({
11110
+ autoCloseOnDone: exports_external.boolean().optional(),
11111
+ postToLinearOnDone: PostWorktreeToLinearTargetSchema.optional()
11112
+ });
11031
11113
  var AgentCapabilitiesSchema = exports_external.object({
11032
11114
  terminal: exports_external.literal(true),
11033
11115
  inAppChat: exports_external.boolean(),
@@ -11084,6 +11166,7 @@ var NumberLikePathParamSchema = exports_external.union([
11084
11166
  var BranchListResponseSchema = exports_external.object({
11085
11167
  branches: exports_external.array(AvailableBranchSchema)
11086
11168
  });
11169
+ var WorktreeSourceSchema = exports_external.enum(["ui", "oneshot"]);
11087
11170
  var CreateWorktreeRequestSchema = exports_external.object({
11088
11171
  mode: WorktreeCreateModeSchema.optional(),
11089
11172
  branch: exports_external.string().optional(),
@@ -11094,7 +11177,14 @@ var CreateWorktreeRequestSchema = exports_external.object({
11094
11177
  prompt: exports_external.string().optional(),
11095
11178
  envOverrides: exports_external.record(exports_external.string()).optional(),
11096
11179
  createLinearTicket: exports_external.literal(true).optional(),
11097
- linearTitle: exports_external.string().optional()
11180
+ linearTitle: exports_external.string().optional(),
11181
+ fromLinear: FromLinearInputSchema.optional(),
11182
+ source: WorktreeSourceSchema.optional(),
11183
+ oneshot: OneshotConfigSchema.optional()
11184
+ });
11185
+ var OpenWorktreeRequestSchema = exports_external.object({
11186
+ prompt: exports_external.string().optional(),
11187
+ oneshot: OneshotConfigSchema.optional()
11098
11188
  });
11099
11189
  var CreateWorktreeResponseSchema = exports_external.object({
11100
11190
  primaryBranch: exports_external.string(),
@@ -11240,7 +11330,9 @@ var ProjectWorktreeSnapshotSchema = exports_external.object({
11240
11330
  services: exports_external.array(ServiceStatusSchema),
11241
11331
  prs: exports_external.array(PrEntrySchema),
11242
11332
  linearIssue: LinkedLinearIssueSchema.nullable(),
11243
- creation: WorktreeCreationStateSchema.nullable()
11333
+ creation: WorktreeCreationStateSchema.nullable(),
11334
+ source: WorktreeSourceSchema,
11335
+ oneshot: OneshotConfigSchema.nullable()
11244
11336
  });
11245
11337
  var ProjectSnapshotSchema = exports_external.object({
11246
11338
  project: exports_external.object({
@@ -11289,13 +11381,16 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
11289
11381
  });
11290
11382
  var AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
11291
11383
  var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
11384
+ var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
11292
11385
  var AgentsUiConversationMessageSchema = exports_external.object({
11293
11386
  id: exports_external.string(),
11294
11387
  turnId: exports_external.string(),
11295
11388
  role: AgentsUiConversationMessageRoleSchema,
11296
11389
  text: exports_external.string(),
11297
11390
  status: AgentsUiConversationMessageStatusSchema,
11298
- createdAt: exports_external.string().nullable()
11391
+ createdAt: exports_external.string().nullable(),
11392
+ kind: AgentsUiConversationMessageKindSchema.optional(),
11393
+ toolName: exports_external.string().optional()
11299
11394
  });
11300
11395
  var AgentsUiConversationStateSchema = exports_external.object({
11301
11396
  provider: WorktreeConversationProviderSchema,
@@ -11419,6 +11514,8 @@ var apiPaths = {
11419
11514
  openWorktree: "/api/worktrees/:name/open",
11420
11515
  closeWorktree: "/api/worktrees/:name/close",
11421
11516
  setWorktreeArchived: "/api/worktrees/:name/archive",
11517
+ syncWorktreePrs: "/api/worktrees/:name/sync-prs",
11518
+ postWorktreeToLinear: "/api/worktrees/:name/linear/post",
11422
11519
  setWorktreeLabel: "/api/worktrees/:name/label",
11423
11520
  sendWorktreePrompt: "/api/worktrees/:name/send",
11424
11521
  mergeWorktree: "/api/worktrees/:name/merge",
@@ -11597,7 +11694,7 @@ var apiContract = c.router({
11597
11694
  method: "POST",
11598
11695
  path: apiPaths.openWorktree,
11599
11696
  pathParams: WorktreeNameParamsSchema,
11600
- body: c.noBody(),
11697
+ body: OpenWorktreeRequestSchema,
11601
11698
  responses: {
11602
11699
  200: OkResponseSchema,
11603
11700
  ...commonErrorResponses
@@ -11623,6 +11720,26 @@ var apiContract = c.router({
11623
11720
  ...commonErrorResponses
11624
11721
  }
11625
11722
  },
11723
+ postWorktreeToLinear: {
11724
+ method: "POST",
11725
+ path: apiPaths.postWorktreeToLinear,
11726
+ pathParams: WorktreeNameParamsSchema,
11727
+ body: PostWorktreeToLinearRequestSchema,
11728
+ responses: {
11729
+ 200: PostWorktreeToLinearResponseSchema,
11730
+ ...commonErrorResponses
11731
+ }
11732
+ },
11733
+ syncWorktreePrs: {
11734
+ method: "POST",
11735
+ path: apiPaths.syncWorktreePrs,
11736
+ pathParams: WorktreeNameParamsSchema,
11737
+ body: c.noBody(),
11738
+ responses: {
11739
+ 200: ProjectWorktreeSnapshotSchema,
11740
+ ...commonErrorResponses
11741
+ }
11742
+ },
11626
11743
  setWorktreeLabel: {
11627
11744
  method: "PUT",
11628
11745
  path: apiPaths.setWorktreeLabel,
@@ -12066,26 +12183,43 @@ function isRecord(raw) {
12066
12183
  function readString(raw) {
12067
12184
  return typeof raw === "string" && raw.length > 0 ? raw : null;
12068
12185
  }
12069
- function extractClaudeMessageText(raw) {
12070
- if (typeof raw === "string") {
12071
- return raw.trim();
12072
- }
12073
- if (!Array.isArray(raw)) {
12074
- return "";
12186
+ var TOOL_PAYLOAD_TRUNCATE_LIMIT = 2000;
12187
+ function compactJson(value) {
12188
+ try {
12189
+ return JSON.stringify(value);
12190
+ } catch {
12191
+ return String(value);
12075
12192
  }
12076
- return raw.map((entry) => {
12193
+ }
12194
+ function truncate(text, limit = TOOL_PAYLOAD_TRUNCATE_LIMIT) {
12195
+ if (text.length <= limit)
12196
+ return text;
12197
+ return `${text.slice(0, limit)}\u2026 (truncated, ${text.length - limit} more chars)`;
12198
+ }
12199
+ function extractToolResultText(content) {
12200
+ if (typeof content === "string")
12201
+ return truncate(content.trim());
12202
+ if (!Array.isArray(content))
12203
+ return truncate(compactJson(content));
12204
+ const text = content.map((entry) => {
12077
12205
  if (!isRecord(entry))
12078
12206
  return "";
12079
- if (entry.type !== "text")
12080
- return "";
12081
- return typeof entry.text === "string" ? entry.text : "";
12207
+ if (entry.type === "text" && typeof entry.text === "string")
12208
+ return entry.text;
12209
+ return compactJson(entry);
12082
12210
  }).join("").trim();
12211
+ return truncate(text);
12083
12212
  }
12084
12213
  function isTopLevelClaudeUserPrompt(raw) {
12085
12214
  if (raw.type !== "user" || !isRecord(raw.message))
12086
12215
  return false;
12087
12216
  return raw.message.role === "user" && typeof raw.message.content === "string" && typeof raw.uuid === "string" && raw.message.content.trim().length > 0;
12088
12217
  }
12218
+ function isClaudeUserToolResultRecord(raw) {
12219
+ if (raw.type !== "user" || !isRecord(raw.message))
12220
+ return false;
12221
+ return raw.message.role === "user" && Array.isArray(raw.message.content) && typeof raw.uuid === "string";
12222
+ }
12089
12223
  function isClaudeAssistantRecord(raw) {
12090
12224
  if (raw.type !== "assistant" || !isRecord(raw.message))
12091
12225
  return false;
@@ -12132,11 +12266,10 @@ async function findClaudeSessionPath(sessionId, cwd) {
12132
12266
  }
12133
12267
  function parseClaudeSessionRecords(text) {
12134
12268
  return text.split(`
12135
- `).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
12269
+ `).map((line) => line.trim()).filter((line) => line.length > 0 && line.startsWith("{")).flatMap((line) => {
12136
12270
  try {
12137
- const parsed = JSON.parse(line);
12138
- return [parsed];
12139
- } catch (error) {
12271
+ return [JSON.parse(line)];
12272
+ } catch {
12140
12273
  log.warn(`[agents] failed to parse Claude session line: ${line.slice(0, 120)}`);
12141
12274
  return [];
12142
12275
  }
@@ -12144,12 +12277,17 @@ function parseClaudeSessionRecords(text) {
12144
12277
  }
12145
12278
  function buildClaudeSessionFromText(input) {
12146
12279
  const records = parseClaudeSessionRecords(input.text);
12147
- const turns = [];
12280
+ const messages = [];
12148
12281
  let cwd = null;
12149
12282
  let gitBranch = null;
12150
12283
  let createdAt = null;
12151
12284
  let lastSeenAt = null;
12152
- let currentTurn = null;
12285
+ let currentTurnId = null;
12286
+ let blockIndex = 0;
12287
+ const pushMessage = (message) => {
12288
+ messages.push(message);
12289
+ blockIndex += 1;
12290
+ };
12153
12291
  for (const record of records) {
12154
12292
  cwd ??= readString(record.cwd);
12155
12293
  gitBranch ??= readString(record.gitBranch);
@@ -12158,38 +12296,74 @@ function buildClaudeSessionFromText(input) {
12158
12296
  }
12159
12297
  lastSeenAt = readString(record.timestamp) ?? lastSeenAt;
12160
12298
  if (isTopLevelClaudeUserPrompt(record)) {
12161
- if (currentTurn) {
12162
- turns.push(currentTurn);
12163
- }
12164
- currentTurn = {
12165
- user: {
12166
- id: record.uuid,
12167
- turnId: record.uuid,
12299
+ currentTurnId = record.uuid;
12300
+ blockIndex = 0;
12301
+ pushMessage({
12302
+ id: record.uuid,
12303
+ turnId: record.uuid,
12304
+ role: "user",
12305
+ kind: "text",
12306
+ text: record.message.content.trim(),
12307
+ createdAt: readString(record.timestamp)
12308
+ });
12309
+ continue;
12310
+ }
12311
+ if (!currentTurnId)
12312
+ continue;
12313
+ if (isClaudeUserToolResultRecord(record)) {
12314
+ for (const entry of record.message.content) {
12315
+ if (!isRecord(entry) || entry.type !== "tool_result")
12316
+ continue;
12317
+ const text = extractToolResultText(entry.content);
12318
+ if (text.length === 0)
12319
+ continue;
12320
+ pushMessage({
12321
+ id: `${record.uuid}:${blockIndex}`,
12322
+ turnId: currentTurnId,
12168
12323
  role: "user",
12169
- text: record.message.content.trim(),
12324
+ kind: "toolResult",
12325
+ text,
12170
12326
  createdAt: readString(record.timestamp)
12171
- },
12172
- assistant: null
12173
- };
12327
+ });
12328
+ }
12174
12329
  continue;
12175
12330
  }
12176
- if (!currentTurn || !isClaudeAssistantRecord(record)) {
12331
+ if (!isClaudeAssistantRecord(record))
12177
12332
  continue;
12178
- }
12179
- const text = extractClaudeMessageText(record.message.content);
12180
- if (text.length === 0 || record.message.stop_reason !== "end_turn") {
12333
+ if (!Array.isArray(record.message.content))
12181
12334
  continue;
12335
+ for (const block of record.message.content) {
12336
+ if (!isRecord(block))
12337
+ continue;
12338
+ if (block.type === "text" && typeof block.text === "string") {
12339
+ const text = block.text.trim();
12340
+ if (text.length === 0)
12341
+ continue;
12342
+ pushMessage({
12343
+ id: `${record.uuid}:${blockIndex}`,
12344
+ turnId: currentTurnId,
12345
+ role: "assistant",
12346
+ kind: "text",
12347
+ text,
12348
+ createdAt: readString(record.timestamp)
12349
+ });
12350
+ continue;
12351
+ }
12352
+ if (block.type === "tool_use") {
12353
+ const toolName = typeof block.name === "string" ? block.name : "tool";
12354
+ const text = truncate(compactJson(block.input ?? {}));
12355
+ pushMessage({
12356
+ id: `${record.uuid}:${blockIndex}`,
12357
+ turnId: currentTurnId,
12358
+ role: "assistant",
12359
+ kind: "toolUse",
12360
+ toolName,
12361
+ text,
12362
+ createdAt: readString(record.timestamp)
12363
+ });
12364
+ continue;
12365
+ }
12182
12366
  }
12183
- currentTurn.assistant = {
12184
- id: record.uuid,
12185
- turnId: currentTurn.user.turnId,
12186
- role: "assistant",
12187
- text,
12188
- createdAt: readString(record.timestamp)
12189
- };
12190
- }
12191
- if (currentTurn) {
12192
- turns.push(currentTurn);
12193
12367
  }
12194
12368
  return {
12195
12369
  sessionId: input.sessionId,
@@ -12198,7 +12372,7 @@ function buildClaudeSessionFromText(input) {
12198
12372
  gitBranch,
12199
12373
  createdAt,
12200
12374
  lastSeenAt,
12201
- messages: turns.flatMap((turn) => turn.assistant ? [turn.user, turn.assistant] : [turn.user])
12375
+ messages
12202
12376
  };
12203
12377
  }
12204
12378
 
@@ -12824,6 +12998,15 @@ var DEFAULT_PANES = [
12824
12998
  { id: "agent", kind: "agent", focus: true },
12825
12999
  { id: "shell", kind: "shell", split: "right", sizePct: 25 }
12826
13000
  ];
13001
+ function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
13002
+ return [
13003
+ "You are running in webmux ONESHOT mode. There is NO interactive user \u2014 nobody is watching the chat or will respond to questions, approvals, or status checks. Any message asking the user to review, approve, confirm, take a look, or 'let you know' is wasted output: it will not be answered.",
13004
+ "Your job is to take the task to its real conclusion without pausing:",
13005
+ "1) Make the change. 2) Validate it (run the relevant tests, typecheck, build, or quick manual check). 3) Commit. 4) Push. 5) Open a pull request. Only then are you done.",
13006
+ "When something is ambiguous, pick the most reasonable default and proceed. When you would normally ask 'should I X or Y?', just pick one and continue \u2014 note the choice in the PR description if it matters.",
13007
+ "Never end your turn with a question, a suggestion to 'take a look', or a request for approval. Stop only when the PR is open, or when you hit a technical error you cannot recover from yourself (in which case clearly state the blocker)."
13008
+ ].join(" ");
13009
+ }
12827
13010
  var DEFAULT_CONFIG = {
12828
13011
  name: "Webmux",
12829
13012
  workspace: {
@@ -12847,7 +13030,8 @@ var DEFAULT_CONFIG = {
12847
13030
  linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
12848
13031
  },
12849
13032
  lifecycleHooks: {},
12850
- autoName: null
13033
+ autoName: null,
13034
+ oneshot: { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() }
12851
13035
  };
12852
13036
  function clonePanes(panes) {
12853
13037
  return panes.map((pane) => ({ ...pane }));
@@ -13025,6 +13209,12 @@ function parseLifecycleHooks(raw) {
13025
13209
  }
13026
13210
  return hooks;
13027
13211
  }
13212
+ function parseOneshot(raw) {
13213
+ if (!isRecord3(raw))
13214
+ return { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() };
13215
+ const systemPrompt = typeof raw.systemPrompt === "string" && raw.systemPrompt.trim() ? raw.systemPrompt.trim() : DEFAULT_ONESHOT_SYSTEM_PROMPT();
13216
+ return { systemPrompt };
13217
+ }
13028
13218
  function parseAutoName(raw) {
13029
13219
  if (!isRecord3(raw))
13030
13220
  return null;
@@ -13097,7 +13287,8 @@ function parseProjectConfig(parsed) {
13097
13287
  }
13098
13288
  },
13099
13289
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
13100
- autoName: parseAutoName(parsed.auto_name)
13290
+ autoName: parseAutoName(parsed.auto_name),
13291
+ oneshot: parseOneshot(parsed.oneshot)
13101
13292
  };
13102
13293
  }
13103
13294
  function defaultConfig() {
@@ -13603,6 +13794,46 @@ function validateCustomAgentInput(input) {
13603
13794
  }
13604
13795
 
13605
13796
  // backend/src/services/linear-service.ts
13797
+ import { request as httpsRequest } from "https";
13798
+ function getHeader(headers, name) {
13799
+ const lower = name.toLowerCase();
13800
+ for (const [key, value] of Object.entries(headers)) {
13801
+ if (key.toLowerCase() === lower)
13802
+ return value;
13803
+ }
13804
+ return;
13805
+ }
13806
+ function hasHeader(headers, name) {
13807
+ return getHeader(headers, name) !== undefined;
13808
+ }
13809
+ function putViaNodeHttps(url, headers, body) {
13810
+ return new Promise((resolve3, reject) => {
13811
+ const parsed = new URL(url);
13812
+ const req = httpsRequest({
13813
+ method: "PUT",
13814
+ hostname: parsed.hostname,
13815
+ port: parsed.port || 443,
13816
+ path: parsed.pathname + parsed.search,
13817
+ headers: {
13818
+ ...headers,
13819
+ "Content-Length": String(body.byteLength)
13820
+ }
13821
+ }, (res) => {
13822
+ const chunks = [];
13823
+ res.on("data", (chunk) => chunks.push(chunk));
13824
+ res.on("end", () => {
13825
+ resolve3({
13826
+ status: res.statusCode ?? 0,
13827
+ body: Buffer.concat(chunks).toString("utf8")
13828
+ });
13829
+ });
13830
+ res.on("error", reject);
13831
+ });
13832
+ req.on("error", reject);
13833
+ req.write(Buffer.from(body));
13834
+ req.end();
13835
+ });
13836
+ }
13606
13837
  var ASSIGNED_ISSUES_QUERY = `
13607
13838
  query AssignedIssues {
13608
13839
  viewer {
@@ -13897,6 +14128,278 @@ async function fetchAssignedIssues(options) {
13897
14128
  }
13898
14129
  return result;
13899
14130
  }
14131
+ var ISSUE_WITH_ATTACHMENTS_QUERY = `
14132
+ query IssueWithAttachments($id: String!) {
14133
+ issue(id: $id) {
14134
+ id
14135
+ identifier
14136
+ title
14137
+ description
14138
+ url
14139
+ branchName
14140
+ attachments {
14141
+ nodes {
14142
+ id
14143
+ url
14144
+ title
14145
+ subtitle
14146
+ sourceType
14147
+ metadata
14148
+ createdAt
14149
+ }
14150
+ }
14151
+ }
14152
+ }
14153
+ `;
14154
+ var TEAM_BY_KEY_QUERY = `
14155
+ query TeamByKey($key: String!) {
14156
+ teams(filter: { key: { eq: $key } }, first: 1) {
14157
+ nodes {
14158
+ id
14159
+ key
14160
+ name
14161
+ }
14162
+ }
14163
+ }
14164
+ `;
14165
+ var FILE_UPLOAD_MUTATION = `
14166
+ mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) {
14167
+ fileUpload(contentType: $contentType, filename: $filename, size: $size) {
14168
+ success
14169
+ uploadFile {
14170
+ uploadUrl
14171
+ assetUrl
14172
+ headers {
14173
+ key
14174
+ value
14175
+ }
14176
+ }
14177
+ }
14178
+ }
14179
+ `;
14180
+ var ATTACHMENT_CREATE_MUTATION = `
14181
+ mutation AttachmentCreate($issueId: String!, $title: String!, $url: String!, $subtitle: String) {
14182
+ attachmentCreate(input: { issueId: $issueId, title: $title, url: $url, subtitle: $subtitle }) {
14183
+ success
14184
+ attachment {
14185
+ id
14186
+ url
14187
+ }
14188
+ }
14189
+ }
14190
+ `;
14191
+ var COMMENT_CREATE_MUTATION = `
14192
+ mutation CommentCreate($issueId: String!, $body: String!) {
14193
+ commentCreate(input: { issueId: $issueId, body: $body }) {
14194
+ success
14195
+ comment {
14196
+ id
14197
+ url
14198
+ }
14199
+ }
14200
+ }
14201
+ `;
14202
+ var WEBMUX_ATTACHMENT_TITLE_PREFIX = "webmux-state:";
14203
+ function buildWebmuxAttachmentTitle(branch) {
14204
+ return `${WEBMUX_ATTACHMENT_TITLE_PREFIX}${branch}`;
14205
+ }
14206
+ function findWebmuxAttachment(issue, branch) {
14207
+ const candidates = issue.attachments.filter((a) => a.title.startsWith(WEBMUX_ATTACHMENT_TITLE_PREFIX));
14208
+ if (candidates.length === 0)
14209
+ return null;
14210
+ if (branch) {
14211
+ const exact = candidates.find((a) => a.title === buildWebmuxAttachmentTitle(branch));
14212
+ if (exact)
14213
+ return exact;
14214
+ }
14215
+ return [...candidates].sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] ?? null;
14216
+ }
14217
+ function inferPrStateFromAttachment(attachment) {
14218
+ const meta = attachment.metadata ?? {};
14219
+ const rawState = typeof meta.state === "string" ? meta.state.toLowerCase() : null;
14220
+ if (rawState === "open" || rawState === "closed" || rawState === "merged")
14221
+ return rawState;
14222
+ const status = typeof meta.status === "string" ? meta.status.toLowerCase() : null;
14223
+ if (status === "open" || status === "closed" || status === "merged")
14224
+ return status;
14225
+ return "unknown";
14226
+ }
14227
+ function inferPrBranchFromAttachment(attachment) {
14228
+ const meta = attachment.metadata ?? {};
14229
+ if (typeof meta.branchName === "string" && meta.branchName.trim())
14230
+ return meta.branchName.trim();
14231
+ if (typeof meta.headRefName === "string" && meta.headRefName.trim())
14232
+ return meta.headRefName.trim();
14233
+ return null;
14234
+ }
14235
+ var STATE_PRIORITY = {
14236
+ open: 0,
14237
+ merged: 1,
14238
+ closed: 2,
14239
+ unknown: 3
14240
+ };
14241
+ function findLinkedGitHubPr(issue) {
14242
+ const githubAttachments = issue.attachments.filter((a) => {
14243
+ if (a.sourceType === "github" || a.sourceType === "githubPR" || a.sourceType === "github_pull_request") {
14244
+ return true;
14245
+ }
14246
+ return /github\.com\/.+\/pull\/\d+/i.test(a.url);
14247
+ });
14248
+ if (githubAttachments.length === 0)
14249
+ return null;
14250
+ const prs = githubAttachments.map((a) => ({
14251
+ url: a.url,
14252
+ branch: inferPrBranchFromAttachment(a),
14253
+ state: inferPrStateFromAttachment(a)
14254
+ }));
14255
+ const indexed = prs.map((pr, idx) => ({ pr, idx, attachment: githubAttachments[idx] }));
14256
+ indexed.sort((a, b) => {
14257
+ const stateDiff = STATE_PRIORITY[a.pr.state] - STATE_PRIORITY[b.pr.state];
14258
+ if (stateDiff !== 0)
14259
+ return stateDiff;
14260
+ return b.attachment.createdAt.localeCompare(a.attachment.createdAt);
14261
+ });
14262
+ return indexed[0].pr;
14263
+ }
14264
+ function buildLinearSummaryMarkdown(input) {
14265
+ const lines = [
14266
+ `**Webmux session \u2014 branch \`${input.branch}\`**`,
14267
+ ""
14268
+ ];
14269
+ if (input.baseBranch)
14270
+ lines.push(`- Base: \`${input.baseBranch}\``);
14271
+ lines.push(`- Turns: ${input.turns}`);
14272
+ if (input.prUrl)
14273
+ lines.push(`- PR: ${input.prUrl}`);
14274
+ lines.push(`- Transcript: see attachment \`${input.attachmentTitle}\``);
14275
+ if (input.webmuxVersion)
14276
+ lines.push(`- webmux: ${input.webmuxVersion}`);
14277
+ lines.push("");
14278
+ lines.push("_Resume on another machine with_ `webmux oneshot --linear <issue-id>`.");
14279
+ return lines.join(`
14280
+ `);
14281
+ }
14282
+ async function fetchIssueWithAttachments(issueIdentifierOrId) {
14283
+ const response = await postLinearGraphql(ISSUE_WITH_ATTACHMENTS_QUERY, {
14284
+ id: issueIdentifierOrId
14285
+ });
14286
+ if (!response.ok) {
14287
+ return { ok: false, error: response.error, status: 502 };
14288
+ }
14289
+ const error = gqlErrorMessage(response.data);
14290
+ if (error) {
14291
+ return { ok: false, error, status: 502 };
14292
+ }
14293
+ const issue = response.data.data?.issue;
14294
+ if (!issue) {
14295
+ return { ok: false, error: `Linear issue not found: ${issueIdentifierOrId}`, status: 404 };
14296
+ }
14297
+ return {
14298
+ ok: true,
14299
+ data: {
14300
+ id: issue.id,
14301
+ identifier: issue.identifier,
14302
+ title: issue.title,
14303
+ description: issue.description,
14304
+ url: issue.url,
14305
+ branchName: issue.branchName,
14306
+ attachments: issue.attachments.nodes.map((node) => ({
14307
+ id: node.id,
14308
+ url: node.url,
14309
+ title: node.title,
14310
+ subtitle: node.subtitle,
14311
+ sourceType: node.sourceType,
14312
+ metadata: node.metadata,
14313
+ createdAt: node.createdAt
14314
+ }))
14315
+ }
14316
+ };
14317
+ }
14318
+ async function fetchTeamByKey(teamKey) {
14319
+ const response = await postLinearGraphql(TEAM_BY_KEY_QUERY, { key: teamKey });
14320
+ if (!response.ok) {
14321
+ return { ok: false, error: response.error, status: 502 };
14322
+ }
14323
+ const error = gqlErrorMessage(response.data);
14324
+ if (error) {
14325
+ return { ok: false, error, status: 502 };
14326
+ }
14327
+ const team = response.data.data?.teams.nodes[0];
14328
+ if (!team) {
14329
+ return { ok: false, error: `Linear team not found for key: ${teamKey}`, status: 404 };
14330
+ }
14331
+ return { ok: true, data: team };
14332
+ }
14333
+ async function uploadAttachmentFile(input) {
14334
+ const response = await postLinearGraphql(FILE_UPLOAD_MUTATION, {
14335
+ contentType: input.contentType,
14336
+ filename: input.filename,
14337
+ size: input.body.byteLength
14338
+ });
14339
+ if (!response.ok)
14340
+ return { ok: false, error: response.error };
14341
+ const error = gqlErrorMessage(response.data);
14342
+ if (error)
14343
+ return { ok: false, error };
14344
+ const upload = response.data.data?.fileUpload;
14345
+ if (!upload?.success || !upload.uploadFile) {
14346
+ return { ok: false, error: "Linear fileUpload did not return an upload URL" };
14347
+ }
14348
+ const headers = {};
14349
+ for (const h of upload.uploadFile.headers)
14350
+ headers[h.key] = h.value;
14351
+ if (!hasHeader(headers, "content-type")) {
14352
+ headers["Content-Type"] = input.contentType;
14353
+ }
14354
+ if (!hasHeader(headers, "x-goog-content-length-range")) {
14355
+ const size = input.body.byteLength;
14356
+ headers["x-goog-content-length-range"] = `${size},${size}`;
14357
+ }
14358
+ try {
14359
+ const { status, body } = await putViaNodeHttps(upload.uploadFile.uploadUrl, headers, input.body);
14360
+ if (status < 200 || status >= 300) {
14361
+ return { ok: false, error: `Asset upload failed ${status}: ${body.slice(0, 1000)}` };
14362
+ }
14363
+ } catch (err) {
14364
+ const msg = err instanceof Error ? err.message : String(err);
14365
+ return { ok: false, error: `Asset upload error: ${msg}` };
14366
+ }
14367
+ return { ok: true, data: { assetUrl: upload.uploadFile.assetUrl } };
14368
+ }
14369
+ async function attachToIssue(input) {
14370
+ const response = await postLinearGraphql(ATTACHMENT_CREATE_MUTATION, {
14371
+ issueId: input.issueId,
14372
+ title: input.title,
14373
+ url: input.url,
14374
+ subtitle: input.subtitle ?? null
14375
+ });
14376
+ if (!response.ok)
14377
+ return { ok: false, error: response.error };
14378
+ const error = gqlErrorMessage(response.data);
14379
+ if (error)
14380
+ return { ok: false, error };
14381
+ const payload = response.data.data?.attachmentCreate;
14382
+ if (!payload?.success || !payload.attachment) {
14383
+ return { ok: false, error: "Linear attachmentCreate did not succeed" };
14384
+ }
14385
+ return { ok: true, data: payload.attachment };
14386
+ }
14387
+ async function createIssueComment(input) {
14388
+ const response = await postLinearGraphql(COMMENT_CREATE_MUTATION, {
14389
+ issueId: input.issueId,
14390
+ body: input.body
14391
+ });
14392
+ if (!response.ok)
14393
+ return { ok: false, error: response.error };
14394
+ const error = gqlErrorMessage(response.data);
14395
+ if (error)
14396
+ return { ok: false, error };
14397
+ const payload = response.data.data?.commentCreate;
14398
+ if (!payload?.success || !payload.comment) {
14399
+ return { ok: false, error: "Linear commentCreate did not succeed" };
14400
+ }
14401
+ return { ok: true, data: payload.comment };
14402
+ }
13900
14403
  async function createLinearIssue(input) {
13901
14404
  const viewerResult = await fetchViewerId();
13902
14405
  if (!viewerResult.ok) {
@@ -13929,6 +14432,202 @@ async function createLinearIssue(input) {
13929
14432
  return result;
13930
14433
  }
13931
14434
 
14435
+ // backend/src/services/conversation-export-service.ts
14436
+ var WebmuxConversationAttachmentPayloadSchema = exports_external.object({
14437
+ webmux: exports_external.literal(1),
14438
+ branch: exports_external.string(),
14439
+ baseBranch: exports_external.string().nullable(),
14440
+ agent: AgentIdSchema.nullable(),
14441
+ createdAt: exports_external.string(),
14442
+ conversation: exports_external.array(AgentsUiConversationMessageSchema)
14443
+ });
14444
+ var defaultSeedFromLinearDeps = {
14445
+ fetchIssueWithAttachments,
14446
+ downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
14447
+ };
14448
+ function countConversationTurns(conversation) {
14449
+ return new Set(conversation.messages.map((m) => m.turnId)).size;
14450
+ }
14451
+ function escapeFence(text) {
14452
+ return text.replace(/```/g, "``\u200B`");
14453
+ }
14454
+ function buildConversationAttachmentPayload(input) {
14455
+ const now = input.now ?? (() => new Date);
14456
+ return {
14457
+ webmux: 1,
14458
+ branch: input.branch,
14459
+ baseBranch: input.baseBranch,
14460
+ agent: input.agent,
14461
+ createdAt: now().toISOString(),
14462
+ conversation: input.conversation.messages
14463
+ };
14464
+ }
14465
+ async function resolveIssue(input, deps) {
14466
+ if (input.target.kind === "issue") {
14467
+ const issue = await deps.fetchIssueWithAttachments(input.target.issueId);
14468
+ if (!issue.ok)
14469
+ return issue;
14470
+ return { ok: true, issueId: issue.data.id, issueUrl: issue.data.url };
14471
+ }
14472
+ const team = await deps.fetchTeamByKey(input.target.teamKey);
14473
+ if (!team.ok)
14474
+ return team;
14475
+ const titleFromPrompt = input.target.title?.trim();
14476
+ const title = titleFromPrompt && titleFromPrompt.length > 0 ? titleFromPrompt : `Webmux session: ${input.branch}`;
14477
+ const description = [
14478
+ `Created from a webmux session on branch \`${input.branch}\`.`,
14479
+ input.prUrl ? `
14480
+ PR: ${input.prUrl}` : ""
14481
+ ].filter(Boolean).join(`
14482
+ `);
14483
+ const created = await deps.createLinearIssue({
14484
+ teamId: team.data.id,
14485
+ title,
14486
+ description
14487
+ });
14488
+ if (!created.ok)
14489
+ return { ok: false, error: created.error, status: 502 };
14490
+ return { ok: true, issueId: created.data.id, issueUrl: created.data.url };
14491
+ }
14492
+ async function exportConversationToLinear(input, deps) {
14493
+ const issue = await resolveIssue(input, deps);
14494
+ if (!issue.ok)
14495
+ return issue;
14496
+ const payload = buildConversationAttachmentPayload(input);
14497
+ const attachmentTitle = buildWebmuxAttachmentTitle(input.branch);
14498
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(payload, null, 2));
14499
+ const filename = `${attachmentTitle}.json`;
14500
+ const upload = await deps.uploadAttachmentFile({
14501
+ filename,
14502
+ contentType: "application/json",
14503
+ body: bodyBytes.buffer
14504
+ });
14505
+ if (!upload.ok) {
14506
+ return { ok: false, error: `Linear file upload failed: ${upload.error}`, status: 502 };
14507
+ }
14508
+ const attached = await deps.attachToIssue({
14509
+ issueId: issue.issueId,
14510
+ title: attachmentTitle,
14511
+ url: upload.data.assetUrl,
14512
+ subtitle: input.prUrl ?? undefined
14513
+ });
14514
+ if (!attached.ok) {
14515
+ return { ok: false, error: `Linear attachmentCreate failed: ${attached.error}`, status: 502 };
14516
+ }
14517
+ const summary = buildLinearSummaryMarkdown({
14518
+ branch: input.branch,
14519
+ baseBranch: input.baseBranch ?? undefined,
14520
+ turns: countConversationTurns(input.conversation),
14521
+ prUrl: input.prUrl ?? undefined,
14522
+ attachmentTitle,
14523
+ webmuxVersion: input.webmuxVersion
14524
+ });
14525
+ const comment = await deps.createIssueComment({
14526
+ issueId: issue.issueId,
14527
+ body: summary
14528
+ });
14529
+ let commentUrl = null;
14530
+ if (comment.ok) {
14531
+ commentUrl = comment.data.url;
14532
+ } else {
14533
+ log.error(`[linear] comment creation failed (attachment still saved): ${comment.error}`);
14534
+ }
14535
+ return {
14536
+ ok: true,
14537
+ data: {
14538
+ issueId: issue.issueId,
14539
+ issueUrl: issue.issueUrl,
14540
+ attachmentUrl: upload.data.assetUrl,
14541
+ commentUrl
14542
+ }
14543
+ };
14544
+ }
14545
+ function buildIssueHeader(issue) {
14546
+ const lines = [];
14547
+ lines.push(`This worktree is for Linear issue **${issue.identifier}** \u2014 ${issue.url}`);
14548
+ lines.push("");
14549
+ lines.push(`When opening a PR, reference \`Fixes ${issue.identifier}\` in the title or body so Linear links it back automatically (Linear also auto-links PRs on the branch \`${issue.branchName}\`).`);
14550
+ lines.push("");
14551
+ lines.push(`## Issue: ${issue.title}`);
14552
+ if (issue.description?.trim()) {
14553
+ lines.push("");
14554
+ lines.push(escapeFence(issue.description.trim()));
14555
+ }
14556
+ lines.push("");
14557
+ return lines.join(`
14558
+ `);
14559
+ }
14560
+ function buildPriorConversationSection(payload) {
14561
+ const lines = [];
14562
+ lines.push(`---`);
14563
+ lines.push("");
14564
+ lines.push(`A previous webmux session for this issue was saved here (branch \`${payload.branch}\`${payload.baseBranch ? `, base \`${payload.baseBranch}\`` : ""}).`);
14565
+ lines.push("");
14566
+ lines.push("Previous conversation (chronological):");
14567
+ lines.push("");
14568
+ for (const message of payload.conversation) {
14569
+ lines.push(`### ${message.role}`);
14570
+ lines.push("");
14571
+ lines.push(escapeFence(message.text));
14572
+ lines.push("");
14573
+ }
14574
+ return lines.join(`
14575
+ `);
14576
+ }
14577
+ async function buildSeedFromLinear(input, deps) {
14578
+ const issue = await deps.fetchIssueWithAttachments(input.issueId);
14579
+ if (!issue.ok)
14580
+ return issue;
14581
+ const issueHeader = buildIssueHeader(issue.data);
14582
+ const webmuxAttachment = findWebmuxAttachment(issue.data, input.preferBranch);
14583
+ const pr = findLinkedGitHubPr(issue.data);
14584
+ let attachmentPayload = null;
14585
+ if (webmuxAttachment) {
14586
+ const payloadResult = await deps.downloadWebmuxAttachment(webmuxAttachment.url);
14587
+ if (payloadResult.ok) {
14588
+ attachmentPayload = payloadResult.data;
14589
+ } else {
14590
+ log.error(`[linear] webmux attachment download failed: ${payloadResult.error}`);
14591
+ }
14592
+ }
14593
+ const source = attachmentPayload ? "webmux-attachment" : pr ? "github-integration" : "none";
14594
+ const branch = attachmentPayload?.branch ?? pr?.branch ?? (issue.data.branchName || null);
14595
+ const baseBranch = attachmentPayload?.baseBranch ?? null;
14596
+ const conversationMarkdown = attachmentPayload ? `${issueHeader}${buildPriorConversationSection(attachmentPayload)}` : issueHeader;
14597
+ return {
14598
+ ok: true,
14599
+ data: {
14600
+ source,
14601
+ branch,
14602
+ baseBranch,
14603
+ prUrl: pr?.url ?? null,
14604
+ conversationMarkdown
14605
+ }
14606
+ };
14607
+ }
14608
+ async function downloadWebmuxAttachmentDefault(url) {
14609
+ const apiKey = Bun.env.LINEAR_API_KEY;
14610
+ if (!apiKey)
14611
+ return { ok: false, error: "LINEAR_API_KEY not set" };
14612
+ try {
14613
+ const res = await fetch(url, {
14614
+ headers: { Authorization: apiKey }
14615
+ });
14616
+ if (!res.ok) {
14617
+ return { ok: false, error: `Asset download failed ${res.status}` };
14618
+ }
14619
+ const text = await res.text();
14620
+ const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
14621
+ if (!parsed.success) {
14622
+ return { ok: false, error: "Asset is not a webmux conversation payload" };
14623
+ }
14624
+ return { ok: true, data: parsed.data };
14625
+ } catch (err) {
14626
+ const msg = err instanceof Error ? err.message : String(err);
14627
+ return { ok: false, error: msg };
14628
+ }
14629
+ }
14630
+
13932
14631
  // backend/src/services/lifecycle-service.ts
13933
14632
  import { mkdir as mkdir4 } from "fs/promises";
13934
14633
  import { dirname as dirname4, resolve as resolve7 } from "path";
@@ -14797,23 +15496,22 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
14797
15496
  return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
14798
15497
  }
14799
15498
  function buildBuiltInAgentInvocation(input) {
15499
+ const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
14800
15500
  if (input.agent === "codex") {
14801
15501
  const hooksFlag = " --enable codex_hooks";
14802
15502
  const yoloFlag2 = input.yolo ? " --yolo" : "";
14803
15503
  if (input.launchMode === "resume") {
14804
- return `codex${hooksFlag}${yoloFlag2} resume --last`;
15504
+ return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
14805
15505
  }
14806
- const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
14807
15506
  if (input.systemPrompt) {
14808
- return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
15507
+ return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
14809
15508
  }
14810
- return `codex${hooksFlag}${yoloFlag2}${promptSuffix2}`;
15509
+ return `codex${hooksFlag}${yoloFlag2}${promptSuffix}`;
14811
15510
  }
14812
15511
  const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
14813
15512
  if (input.launchMode === "resume") {
14814
- return `claude${yoloFlag} --continue`;
15513
+ return `claude${yoloFlag} --continue${promptSuffix}`;
14815
15514
  }
14816
- const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
14817
15515
  if (input.systemPrompt) {
14818
15516
  return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
14819
15517
  }
@@ -14975,12 +15673,26 @@ import { randomUUID } from "crypto";
14975
15673
  // backend/src/adapters/git.ts
14976
15674
  import { readdirSync, rmSync, statSync } from "fs";
14977
15675
  import { resolve as resolve6, join as join5 } from "path";
15676
+ function spawnGit(args, cwd) {
15677
+ try {
15678
+ return {
15679
+ ok: true,
15680
+ result: Bun.spawnSync(["git", ...args], {
15681
+ cwd,
15682
+ stdout: "pipe",
15683
+ stderr: "pipe"
15684
+ })
15685
+ };
15686
+ } catch (error) {
15687
+ return { ok: false, stderr: `spawn error (cwd=${cwd}): ${errorMessage(error)}` };
15688
+ }
15689
+ }
14978
15690
  function runGit(args, cwd) {
14979
- const result = Bun.spawnSync(["git", ...args], {
14980
- cwd,
14981
- stdout: "pipe",
14982
- stderr: "pipe"
14983
- });
15691
+ const spawned = spawnGit(args, cwd);
15692
+ if (!spawned.ok) {
15693
+ throw new Error(`git ${args.join(" ")} failed: ${spawned.stderr}`);
15694
+ }
15695
+ const { result } = spawned;
14984
15696
  if (result.exitCode !== 0) {
14985
15697
  const stderr = new TextDecoder().decode(result.stderr).trim();
14986
15698
  throw new Error(`git ${args.join(" ")} failed: ${stderr || `exit ${result.exitCode}`}`);
@@ -14988,11 +15700,11 @@ function runGit(args, cwd) {
14988
15700
  return new TextDecoder().decode(result.stdout).trim();
14989
15701
  }
14990
15702
  function tryRunGit(args, cwd) {
14991
- const result = Bun.spawnSync(["git", ...args], {
14992
- cwd,
14993
- stdout: "pipe",
14994
- stderr: "pipe"
14995
- });
15703
+ const spawned = spawnGit(args, cwd);
15704
+ if (!spawned.ok) {
15705
+ return { ok: false, stderr: spawned.stderr };
15706
+ }
15707
+ const { result } = spawned;
14996
15708
  if (result.exitCode !== 0) {
14997
15709
  return {
14998
15710
  ok: false,
@@ -15113,6 +15825,16 @@ function listGitWorktrees(cwd) {
15113
15825
  const output = runGit(["worktree", "list", "--porcelain"], cwd);
15114
15826
  return parseGitWorktreePorcelain(output);
15115
15827
  }
15828
+ function worktreeEntryPathExists(entry) {
15829
+ try {
15830
+ return statSync(entry.path).isDirectory();
15831
+ } catch {
15832
+ return false;
15833
+ }
15834
+ }
15835
+ function filterLiveWorktreeEntries(entries) {
15836
+ return entries.filter(worktreeEntryPathExists);
15837
+ }
15116
15838
  function listLocalGitBranches(cwd) {
15117
15839
  const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd);
15118
15840
  return output.split(`
@@ -15173,6 +15895,9 @@ class BunGitGateway {
15173
15895
  listWorktrees(cwd) {
15174
15896
  return listGitWorktrees(cwd);
15175
15897
  }
15898
+ listLiveWorktrees(cwd) {
15899
+ return filterLiveWorktreeEntries(listGitWorktrees(cwd));
15900
+ }
15176
15901
  listLocalBranches(cwd) {
15177
15902
  return listLocalGitBranches(cwd);
15178
15903
  }
@@ -15326,7 +16051,9 @@ async function initializeManagedWorktree(opts) {
15326
16051
  agent: opts.agent,
15327
16052
  runtime: opts.runtime,
15328
16053
  startupEnvValues: { ...opts.startupEnvValues ?? {} },
15329
- allocatedPorts: { ...opts.allocatedPorts ?? {} }
16054
+ allocatedPorts: { ...opts.allocatedPorts ?? {} },
16055
+ ...opts.source ? { source: opts.source } : {},
16056
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
15330
16057
  };
15331
16058
  const paths = await ensureWorktreeStorageDirs(opts.gitDir);
15332
16059
  await writeWorktreeMeta(opts.gitDir, meta);
@@ -15379,7 +16106,9 @@ async function createManagedWorktree(opts, deps = {}) {
15379
16106
  controlUrl: opts.controlUrl,
15380
16107
  controlToken: opts.controlToken,
15381
16108
  now: opts.now,
15382
- worktreeId: opts.worktreeId
16109
+ worktreeId: opts.worktreeId,
16110
+ ...opts.source ? { source: opts.source } : {},
16111
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
15383
16112
  });
15384
16113
  if (deps.tmux) {
15385
16114
  sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
@@ -15528,10 +16257,15 @@ class LifecycleService {
15528
16257
  agent: agent.id
15529
16258
  });
15530
16259
  }
15531
- async openWorktree(branch) {
16260
+ async openWorktree(branch, options = {}) {
15532
16261
  try {
15533
16262
  const resolved = await this.resolveExistingWorktree(branch);
15534
- const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
16263
+ let initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
16264
+ if (options.oneshot) {
16265
+ const nextMeta = { ...initialized.meta, oneshot: options.oneshot };
16266
+ await writeWorktreeMeta(initialized.paths.gitDir, nextMeta);
16267
+ initialized = { ...initialized, meta: nextMeta };
16268
+ }
15535
16269
  const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
15536
16270
  const agent = this.resolveAgentDefinition(initialized.meta.agent);
15537
16271
  const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
@@ -15546,7 +16280,8 @@ class LifecycleService {
15546
16280
  agent,
15547
16281
  initialized,
15548
16282
  worktreePath: resolved.entry.path,
15549
- launchMode
16283
+ launchMode,
16284
+ followUpPrompt: options.prompt
15550
16285
  });
15551
16286
  await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
15552
16287
  return {
@@ -15557,6 +16292,20 @@ class LifecycleService {
15557
16292
  throw this.wrapOperationError(error);
15558
16293
  }
15559
16294
  }
16295
+ async disarmOneshot(branch) {
16296
+ let resolved;
16297
+ try {
16298
+ resolved = await this.resolveExistingWorktree(branch);
16299
+ } catch {
16300
+ return false;
16301
+ }
16302
+ if (!resolved.meta?.oneshot)
16303
+ return false;
16304
+ const nextMeta = { ...resolved.meta };
16305
+ delete nextMeta.oneshot;
16306
+ await writeWorktreeMeta(resolved.gitDir, nextMeta);
16307
+ return true;
16308
+ }
15560
16309
  async closeWorktree(branch) {
15561
16310
  try {
15562
16311
  await this.resolveExistingWorktree(branch);
@@ -15736,7 +16485,7 @@ class LifecycleService {
15736
16485
  }
15737
16486
  listProjectWorktrees() {
15738
16487
  const projectRoot2 = resolve7(this.deps.projectRoot);
15739
- return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve7(entry.path) !== projectRoot2);
16488
+ return this.deps.git.listLiveWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve7(entry.path) !== projectRoot2);
15740
16489
  }
15741
16490
  async readManagedMetas() {
15742
16491
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -15843,8 +16592,10 @@ class LifecycleService {
15843
16592
  agent: input.agent,
15844
16593
  initialized: input.initialized,
15845
16594
  worktreePath: input.worktreePath,
15846
- prompt: input.prompt,
16595
+ creationPrompt: input.creationPrompt,
16596
+ followUpPrompt: input.followUpPrompt,
15847
16597
  launchMode: input.launchMode,
16598
+ source: input.source,
15848
16599
  containerName
15849
16600
  }));
15850
16601
  return;
@@ -15856,12 +16607,19 @@ class LifecycleService {
15856
16607
  agent: input.agent,
15857
16608
  initialized: input.initialized,
15858
16609
  worktreePath: input.worktreePath,
15859
- prompt: input.prompt,
15860
- launchMode: input.launchMode
16610
+ creationPrompt: input.creationPrompt,
16611
+ followUpPrompt: input.followUpPrompt,
16612
+ launchMode: input.launchMode,
16613
+ source: input.source
15861
16614
  }));
15862
16615
  }
15863
16616
  buildSessionLayout(input) {
15864
- const systemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
16617
+ const baseSystemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
16618
+ const oneshotPrompt = input.launchMode === "fresh" && input.source === "oneshot" ? this.deps.config.oneshot.systemPrompt : undefined;
16619
+ const systemPrompt = baseSystemPrompt && oneshotPrompt ? `${baseSystemPrompt}
16620
+
16621
+ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
16622
+ const prompt = input.launchMode === "resume" ? input.followUpPrompt : input.creationPrompt;
15865
16623
  const containerName = input.containerName;
15866
16624
  return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
15867
16625
  repoRoot: this.deps.projectRoot,
@@ -15876,7 +16634,7 @@ class LifecycleService {
15876
16634
  profileName: input.profileName,
15877
16635
  yolo: input.profile.yolo === true,
15878
16636
  systemPrompt,
15879
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
16637
+ prompt,
15880
16638
  launchMode: input.launchMode
15881
16639
  }),
15882
16640
  shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
@@ -15890,7 +16648,7 @@ class LifecycleService {
15890
16648
  profileName: input.profileName,
15891
16649
  yolo: input.profile.yolo === true,
15892
16650
  systemPrompt,
15893
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
16651
+ prompt,
15894
16652
  launchMode: input.launchMode
15895
16653
  }),
15896
16654
  shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
@@ -16014,12 +16772,14 @@ class LifecycleService {
16014
16772
  const { profileName, profile } = this.resolveProfile(input.profile);
16015
16773
  const agent = this.resolveAgentDefinition(input.agent);
16016
16774
  const worktreePath = this.resolveWorktreePath(input.branch);
16775
+ const source = input.source ?? "ui";
16017
16776
  const createProgressBase = {
16018
16777
  branch: input.branch,
16019
16778
  ...baseBranch ? { baseBranch } : {},
16020
16779
  path: worktreePath,
16021
16780
  profile: profileName,
16022
- agent: input.agent
16781
+ agent: input.agent,
16782
+ source
16023
16783
  };
16024
16784
  const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
16025
16785
  let initialized = null;
@@ -16044,7 +16804,9 @@ class LifecycleService {
16044
16804
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
16045
16805
  controlUrl: this.controlUrl(profile.runtime),
16046
16806
  controlToken: await this.deps.getControlToken(),
16047
- deleteBranchOnRollback
16807
+ deleteBranchOnRollback,
16808
+ source,
16809
+ ...input.oneshot ? { oneshot: input.oneshot } : {}
16048
16810
  }, {
16049
16811
  git: this.deps.git
16050
16812
  });
@@ -16082,8 +16844,9 @@ class LifecycleService {
16082
16844
  agent,
16083
16845
  initialized,
16084
16846
  worktreePath,
16085
- prompt: input.prompt,
16086
- launchMode: "fresh"
16847
+ creationPrompt: input.prompt,
16848
+ launchMode: "fresh",
16849
+ source
16087
16850
  });
16088
16851
  await this.reportCreateProgress({
16089
16852
  ...createProgressBase,
@@ -16569,17 +17332,27 @@ function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs
16569
17332
  var LINEAR_AUTO_CREATE_POLL_INTERVAL_MS = 60000;
16570
17333
  var processedIssueIds = new Set;
16571
17334
  var AUTO_CREATE_LABEL = "webmux";
16572
- function filterAutoCreateIssues(issues, existingBranches) {
17335
+ var AUTO_ONESHOT_LABEL = "webmux_oneshot";
17336
+ function hasLabel(issue, name) {
17337
+ return issue.labels.some((l) => l.name.toLowerCase() === name);
17338
+ }
17339
+ function filterTriggerableIssues(issues, existingBranches, matchesLabelRule) {
16573
17340
  return issues.filter((issue) => {
16574
17341
  if (issue.state.name !== "Todo")
16575
17342
  return false;
16576
- if (!issue.labels.some((l) => l.name.toLowerCase() === AUTO_CREATE_LABEL))
17343
+ if (!matchesLabelRule(issue))
16577
17344
  return false;
16578
17345
  if (processedIssueIds.has(issue.id))
16579
17346
  return false;
16580
17347
  return !existingBranches.some((branch) => branchMatchesIssue(branch, issue.branchName));
16581
17348
  });
16582
17349
  }
17350
+ function filterAutoCreateIssues(issues, existingBranches) {
17351
+ return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_CREATE_LABEL) && !hasLabel(issue, AUTO_ONESHOT_LABEL));
17352
+ }
17353
+ function filterAutoOneshotIssues(issues, existingBranches) {
17354
+ return filterTriggerableIssues(issues, existingBranches, (issue) => hasLabel(issue, AUTO_ONESHOT_LABEL));
17355
+ }
16583
17356
  async function runLinearAutoCreateOnce(deps) {
16584
17357
  const fetchIssues = deps.fetchIssues ?? fetchAssignedIssues;
16585
17358
  const result = await fetchIssues({ skipCache: true });
@@ -16587,29 +17360,53 @@ async function runLinearAutoCreateOnce(deps) {
16587
17360
  log.error(`[linear-auto-create] failed to fetch issues: ${result.error}`);
16588
17361
  return;
16589
17362
  }
17363
+ const eligibleIssueIds = new Set(result.data.filter((issue) => issue.state.name === "Todo" && (hasLabel(issue, AUTO_CREATE_LABEL) || hasLabel(issue, AUTO_ONESHOT_LABEL))).map((issue) => issue.id));
17364
+ for (const id of processedIssueIds) {
17365
+ if (!eligibleIssueIds.has(id))
17366
+ processedIssueIds.delete(id);
17367
+ }
16590
17368
  const projectRoot2 = deps.projectRoot;
16591
17369
  const existingBranches = deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch);
16592
- const newIssues = filterAutoCreateIssues(result.data, existingBranches);
16593
- if (newIssues.length === 0) {
17370
+ const oneshotIssues = deps.runOneshotForIssue ? filterAutoOneshotIssues(result.data, existingBranches) : [];
17371
+ const createIssues = filterAutoCreateIssues(result.data, existingBranches);
17372
+ if (oneshotIssues.length === 0 && createIssues.length === 0) {
16594
17373
  log.debug(`[linear-auto-create] no new labeled issues (${result.data.length} assigned, ${existingBranches.length} worktrees)`);
16595
17374
  return;
16596
17375
  }
16597
- log.info(`[linear-auto-create] found ${newIssues.length} new issue(s) with "${AUTO_CREATE_LABEL}" label`);
16598
- for (const issue of newIssues) {
16599
- try {
16600
- log.info(`[linear-auto-create] creating worktree for ${issue.identifier}: ${issue.title}`);
16601
- await deps.lifecycleService.createWorktree({
16602
- mode: "new",
16603
- branch: issue.branchName,
16604
- prompt: `${issue.title}
17376
+ if (oneshotIssues.length > 0) {
17377
+ log.info(`[linear-auto-create] found ${oneshotIssues.length} new issue(s) with "${AUTO_ONESHOT_LABEL}" label`);
17378
+ for (const issue of oneshotIssues) {
17379
+ try {
17380
+ log.info(`[linear-auto-create] launching oneshot for ${issue.identifier}: ${issue.title}`);
17381
+ await deps.runOneshotForIssue(issue.identifier);
17382
+ processedIssueIds.add(issue.id);
17383
+ log.info(`[linear-auto-create] launched oneshot for ${issue.identifier}`);
17384
+ } catch (err) {
17385
+ const msg = err instanceof Error ? err.message : String(err);
17386
+ log.error(`[linear-auto-create] failed to launch oneshot for ${issue.identifier}: ${msg}`);
17387
+ processedIssueIds.add(issue.id);
17388
+ }
17389
+ }
17390
+ }
17391
+ if (createIssues.length > 0) {
17392
+ log.info(`[linear-auto-create] found ${createIssues.length} new issue(s) with "${AUTO_CREATE_LABEL}" label`);
17393
+ for (const issue of createIssues) {
17394
+ try {
17395
+ log.info(`[linear-auto-create] creating worktree for ${issue.identifier}: ${issue.title}`);
17396
+ await deps.lifecycleService.createWorktree({
17397
+ mode: "new",
17398
+ branch: issue.branchName,
17399
+ prompt: `${issue.title}
16605
17400
 
16606
17401
  ${issue.description ?? ""}`.trim()
16607
- });
16608
- processedIssueIds.add(issue.id);
16609
- log.info(`[linear-auto-create] created worktree for ${issue.identifier}`);
16610
- } catch (err) {
16611
- const msg = err instanceof Error ? err.message : String(err);
16612
- log.error(`[linear-auto-create] failed to create worktree for ${issue.identifier}: ${msg}`);
17402
+ });
17403
+ processedIssueIds.add(issue.id);
17404
+ log.info(`[linear-auto-create] created worktree for ${issue.identifier}`);
17405
+ } catch (err) {
17406
+ const msg = err instanceof Error ? err.message : String(err);
17407
+ log.error(`[linear-auto-create] failed to create worktree for ${issue.identifier}: ${msg}`);
17408
+ processedIssueIds.add(issue.id);
17409
+ }
16613
17410
  }
16614
17411
  }
16615
17412
  }
@@ -16621,9 +17418,93 @@ function resetProcessedIssues() {
16621
17418
  processedIssueIds.clear();
16622
17419
  }
16623
17420
 
17421
+ // backend/src/services/oneshot-watcher-service.ts
17422
+ var POLL_INTERVAL_MS = 3000;
17423
+ var IDLE_GRACE_MS = 15000;
17424
+ var states = new Map;
17425
+ function getState(branch) {
17426
+ let state = states.get(branch);
17427
+ if (!state) {
17428
+ state = { idleSinceMs: null, inFlight: false };
17429
+ states.set(branch, state);
17430
+ }
17431
+ return state;
17432
+ }
17433
+ async function processWorktree(branch, path, agentLifecycle, hasPr, deps) {
17434
+ const readMeta = deps.readWorktreeMeta ?? readWorktreeMeta;
17435
+ const idleGrace = deps.idleGraceMs ?? IDLE_GRACE_MS;
17436
+ const now = deps.now ?? (() => Date.now());
17437
+ const meta = await readMeta(path);
17438
+ if (!meta?.oneshot) {
17439
+ states.delete(branch);
17440
+ return;
17441
+ }
17442
+ const state = getState(branch);
17443
+ if (state.inFlight)
17444
+ return;
17445
+ const isTerminal = agentLifecycle === "stopped" || agentLifecycle === "error";
17446
+ const needsGrace = agentLifecycle === "idle" || agentLifecycle === "closed";
17447
+ if (!isTerminal && !needsGrace) {
17448
+ state.idleSinceMs = null;
17449
+ return;
17450
+ }
17451
+ if (state.idleSinceMs === null)
17452
+ state.idleSinceMs = now();
17453
+ const stable = isTerminal || now() - state.idleSinceMs >= idleGrace;
17454
+ if (!stable)
17455
+ return;
17456
+ state.inFlight = true;
17457
+ try {
17458
+ const reason = isTerminal ? `agent ${agentLifecycle}` : agentLifecycle === "closed" ? "agent closed without resuming" : hasPr ? "agent idle after opening PR" : "agent idle without opening a PR";
17459
+ log.info(`[oneshot-watcher] ${branch}: ${reason} \u2014 firing end-of-run actions`);
17460
+ if (meta.oneshot.postToLinearOnDone) {
17461
+ try {
17462
+ await deps.postToLinear(branch, meta.oneshot.postToLinearOnDone);
17463
+ log.info(`[oneshot-watcher] ${branch}: posted conversation to Linear`);
17464
+ } catch (error) {
17465
+ const msg = error instanceof Error ? error.message : String(error);
17466
+ log.error(`[oneshot-watcher] ${branch}: post-to-Linear failed \u2014 ${msg}`);
17467
+ }
17468
+ }
17469
+ if (meta.oneshot.autoCloseOnDone) {
17470
+ const fresh = await readMeta(path);
17471
+ if (!fresh?.oneshot) {
17472
+ log.info(`[oneshot-watcher] ${branch}: disarmed during post-to-Linear \u2014 skipping close`);
17473
+ return;
17474
+ }
17475
+ try {
17476
+ await deps.lifecycleService.closeWorktree(branch);
17477
+ log.info(`[oneshot-watcher] ${branch}: closed session`);
17478
+ } catch (error) {
17479
+ const msg = error instanceof Error ? error.message : String(error);
17480
+ log.error(`[oneshot-watcher] ${branch}: close failed \u2014 ${msg}`);
17481
+ }
17482
+ }
17483
+ await deps.lifecycleService.disarmOneshot(branch);
17484
+ const runtimeState = deps.projectRuntime.getWorktreeByBranch(branch);
17485
+ if (runtimeState)
17486
+ deps.projectRuntime.setOneshot(runtimeState.worktreeId, null);
17487
+ } finally {
17488
+ states.delete(branch);
17489
+ }
17490
+ }
17491
+ async function runOneshotWatch(deps) {
17492
+ const worktrees = deps.projectRuntime.listWorktrees();
17493
+ for (const wt of worktrees) {
17494
+ if (wt.source !== "oneshot")
17495
+ continue;
17496
+ const hasPr = wt.prs.length > 0;
17497
+ await processWorktree(wt.branch, wt.path, wt.agent.lifecycle, hasPr, deps);
17498
+ }
17499
+ }
17500
+ function startOneshotWatcher(deps) {
17501
+ log.info("[oneshot-watcher] monitor started");
17502
+ return startSerializedInterval(() => runOneshotWatch(deps), deps.pollIntervalMs ?? POLL_INTERVAL_MS);
17503
+ }
17504
+
16624
17505
  // backend/src/services/auto-remove-service.ts
16625
17506
  async function runAutoRemove(deps) {
16626
- const worktrees = deps.git.listWorktrees(deps.projectRoot).filter((e) => !e.bare && e.branch !== null && e.path !== deps.projectRoot);
17507
+ const worktrees = deps.git.listLiveWorktrees(deps.projectRoot).filter((e) => !e.bare && e.branch !== null && e.path !== deps.projectRoot);
16627
17508
  for (const entry of worktrees) {
16628
17509
  const branch = entry.branch;
16629
17510
  if (deps.isRemoving(branch))
@@ -16833,7 +17714,9 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue,
16833
17714
  services: state.services.map((service) => ({ ...service })),
16834
17715
  prs: state.prs.map((pr) => clonePrEntry(pr)),
16835
17716
  linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
16836
- creation: mapCreationSnapshot(creating)
17717
+ creation: mapCreationSnapshot(creating),
17718
+ source: state.source,
17719
+ oneshot: state.oneshot
16837
17720
  };
16838
17721
  }
16839
17722
  function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
@@ -16856,7 +17739,9 @@ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, find
16856
17739
  services: [],
16857
17740
  prs: [],
16858
17741
  linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
16859
- creation: mapCreationSnapshot(creating)
17742
+ creation: mapCreationSnapshot(creating),
17743
+ source: creating.source,
17744
+ oneshot: null
16860
17745
  };
16861
17746
  }
16862
17747
  function buildWorktreeSnapshots(input) {
@@ -17014,13 +17899,18 @@ class ClaudeConversationService {
17014
17899
  }
17015
17900
  async resolveSession(meta, cwd) {
17016
17901
  const savedSessionId = isClaudeConversationMeta(meta.conversation) ? meta.conversation.sessionId : null;
17902
+ const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
17903
+ if (discovered && discovered.sessionId !== savedSessionId) {
17904
+ const session = await this.deps.claude.readSession(discovered.sessionId, cwd);
17905
+ if (session)
17906
+ return session;
17907
+ }
17017
17908
  if (savedSessionId) {
17018
17909
  const savedSession = await this.deps.claude.readSession(savedSessionId, cwd);
17019
17910
  if (savedSession)
17020
17911
  return savedSession;
17021
17912
  log.warn(`[agents] saved Claude session missing, rediscovering cwd=${cwd} sessionId=${savedSessionId}`);
17022
17913
  }
17023
- const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
17024
17914
  if (!discovered)
17025
17915
  return null;
17026
17916
  return await this.deps.claude.readSession(discovered.sessionId, cwd);
@@ -17534,7 +18424,7 @@ async function removeContainer(branch) {
17534
18424
  }
17535
18425
 
17536
18426
  // backend/src/adapters/hooks.ts
17537
- import { join as join7 } from "path";
18427
+ import { join as join6 } from "path";
17538
18428
  function buildErrorMessage(name, exitCode, stdout, stderr) {
17539
18429
  const output = stderr.trim() || stdout.trim();
17540
18430
  if (output) {
@@ -17559,7 +18449,7 @@ class BunLifecycleHookRunner {
17559
18449
  return this.direnvAvailable;
17560
18450
  }
17561
18451
  async buildCommand(cwd, command) {
17562
- if (this.checkDirenv() && await Bun.file(join7(cwd, ".envrc")).exists()) {
18452
+ if (this.checkDirenv() && await Bun.file(join6(cwd, ".envrc")).exists()) {
17563
18453
  Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
17564
18454
  return ["direnv", "exec", cwd, "bash", "-c", command];
17565
18455
  }
@@ -17979,6 +18869,8 @@ function makeDefaultState(input) {
17979
18869
  path: input.path,
17980
18870
  profile: input.profile ?? null,
17981
18871
  agentName: input.agentName ?? null,
18872
+ source: input.source ?? "ui",
18873
+ oneshot: input.oneshot ?? null,
17982
18874
  git: {
17983
18875
  exists: true,
17984
18876
  branch: input.branch,
@@ -18028,6 +18920,10 @@ class ProjectRuntime {
18028
18920
  existing.agentName = input.agentName ?? existing.agentName;
18029
18921
  if (input.runtime)
18030
18922
  existing.agent.runtime = input.runtime;
18923
+ if (input.source !== undefined)
18924
+ existing.source = input.source;
18925
+ if (input.oneshot !== undefined)
18926
+ existing.oneshot = input.oneshot;
18031
18927
  existing.git.exists = true;
18032
18928
  existing.git.branch = input.branch;
18033
18929
  existing.session.windowName = buildWorktreeWindowName(input.branch);
@@ -18038,6 +18934,11 @@ class ProjectRuntime {
18038
18934
  this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
18039
18935
  return created;
18040
18936
  }
18937
+ setOneshot(worktreeId, oneshot) {
18938
+ const state = this.requireWorktree(worktreeId);
18939
+ state.oneshot = oneshot;
18940
+ return state;
18941
+ }
18041
18942
  removeWorktree(worktreeId) {
18042
18943
  const state = this.worktrees.get(worktreeId);
18043
18944
  if (!state)
@@ -18205,7 +19106,7 @@ class ReconciliationService {
18205
19106
  return await this.inFlight;
18206
19107
  }
18207
19108
  async runReconcile(normalizedRepoRoot) {
18208
- const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
19109
+ const worktrees = this.deps.git.listLiveWorktrees(normalizedRepoRoot);
18209
19110
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
18210
19111
  let windows = [];
18211
19112
  try {
@@ -18231,6 +19132,8 @@ class ReconciliationService {
18231
19132
  profile: meta?.profile ?? null,
18232
19133
  agentName: meta?.agent ?? null,
18233
19134
  runtime: meta?.runtime ?? "host",
19135
+ source: meta?.source ?? "ui",
19136
+ oneshot: meta?.oneshot ?? null,
18234
19137
  git: {
18235
19138
  dirty: gitStatus.dirty,
18236
19139
  aheadCount: gitStatus.aheadCount,
@@ -18263,7 +19166,9 @@ class ReconciliationService {
18263
19166
  path: state.path,
18264
19167
  profile: state.profile,
18265
19168
  agentName: state.agentName,
18266
- runtime: state.runtime
19169
+ runtime: state.runtime,
19170
+ source: state.source,
19171
+ oneshot: state.oneshot
18267
19172
  });
18268
19173
  this.deps.runtime.setGitState(state.worktreeId, {
18269
19174
  exists: true,
@@ -18298,7 +19203,8 @@ class WorktreeCreationTracker {
18298
19203
  path: progress.path,
18299
19204
  profile: progress.profile,
18300
19205
  agentName: progress.agent,
18301
- phase: progress.phase
19206
+ phase: progress.phase,
19207
+ source: progress.source
18302
19208
  };
18303
19209
  this.worktrees.set(progress.branch, next);
18304
19210
  }
@@ -18408,15 +19314,60 @@ var lifecycleService = runtime.lifecycleService;
18408
19314
  var linearAutoCreateEnabled = config.integrations.linear.autoCreateWorktrees;
18409
19315
  var stopLinearAutoCreate = null;
18410
19316
  var autoRemoveOnMergeEnabled = config.integrations.github.autoRemoveOnMerge;
19317
+ async function runOneshotForIssue(issueId) {
19318
+ const seed = await buildSeedFromLinear({ issueId }, defaultSeedFromLinearDeps);
19319
+ if (!seed.ok) {
19320
+ throw new Error(`Linear seed failed for ${issueId}: ${seed.error}`);
19321
+ }
19322
+ const branch = seed.data.branch;
19323
+ if (!branch) {
19324
+ throw new Error(`Linear seed for ${issueId} did not resolve to a branch`);
19325
+ }
19326
+ const mode = seed.data.source !== "none" ? "existing" : "new";
19327
+ const prompt = seed.data.conversationMarkdown?.trim() ?? "";
19328
+ await lifecycleService.createWorktree({
19329
+ mode,
19330
+ branch,
19331
+ ...prompt ? { prompt } : {},
19332
+ source: "oneshot",
19333
+ oneshot: {
19334
+ autoCloseOnDone: true,
19335
+ postToLinearOnDone: { kind: "issue", issueId }
19336
+ }
19337
+ });
19338
+ }
18411
19339
  function startLinearAutoCreate() {
18412
19340
  if (stopLinearAutoCreate)
18413
19341
  return;
18414
19342
  stopLinearAutoCreate = startLinearAutoCreateMonitor({
18415
19343
  lifecycleService,
18416
19344
  git,
18417
- projectRoot: PROJECT_DIR
19345
+ projectRoot: PROJECT_DIR,
19346
+ runOneshotForIssue
18418
19347
  });
18419
19348
  }
19349
+ function normalizeOneshotConfig(input) {
19350
+ if (!input)
19351
+ return;
19352
+ return {
19353
+ autoCloseOnDone: input.autoCloseOnDone ?? true,
19354
+ ...input.postToLinearOnDone ? { postToLinearOnDone: input.postToLinearOnDone } : {}
19355
+ };
19356
+ }
19357
+ async function disarmOneshotIfArmed(branch, reason) {
19358
+ try {
19359
+ const disarmed = await lifecycleService.disarmOneshot(branch);
19360
+ if (!disarmed)
19361
+ return;
19362
+ log.info(`[oneshot-watcher] ${branch}: disarmed by ${reason}`);
19363
+ const state = projectRuntime.getWorktreeByBranch(branch);
19364
+ if (state)
19365
+ projectRuntime.setOneshot(state.worktreeId, null);
19366
+ } catch (error) {
19367
+ const msg = error instanceof Error ? error.message : String(error);
19368
+ log.warn(`[oneshot-watcher] disarm failed for ${branch} (${reason}): ${msg}`);
19369
+ }
19370
+ }
18420
19371
  function stopLinearAutoCreateMonitor() {
18421
19372
  if (stopLinearAutoCreate) {
18422
19373
  stopLinearAutoCreate();
@@ -18610,7 +19561,7 @@ async function hasValidControlToken(req) {
18610
19561
  async function getWorktreeGitDirs() {
18611
19562
  const gitDirs = new Map;
18612
19563
  const projectRoot2 = resolve9(PROJECT_DIR);
18613
- for (const entry of git.listWorktrees(projectRoot2)) {
19564
+ for (const entry of git.listLiveWorktrees(projectRoot2)) {
18614
19565
  if (entry.bare || resolve9(entry.path) === projectRoot2 || !entry.branch)
18615
19566
  continue;
18616
19567
  gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
@@ -18726,6 +19677,7 @@ async function apiGetAgentsWorktreeHistory(branch) {
18726
19677
  }
18727
19678
  async function apiSendAgentsWorktreeMessage(branch, req) {
18728
19679
  touchDashboardActivity();
19680
+ await disarmOneshotIfArmed(branch, "agents-send-message");
18729
19681
  const parsed = await parseJsonBody(req, AgentsSendMessageRequestSchema);
18730
19682
  if (!parsed.ok)
18731
19683
  return parsed.response;
@@ -18758,6 +19710,7 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
18758
19710
  }
18759
19711
  async function apiInterruptAgentsWorktree(branch) {
18760
19712
  touchDashboardActivity();
19713
+ await disarmOneshotIfArmed(branch, "agents-interrupt");
18761
19714
  const resolved = await resolveAgentsWorktree(branch);
18762
19715
  if (!resolved.ok)
18763
19716
  return resolved.response;
@@ -18943,6 +19896,37 @@ async function apiCreateWorktree(req) {
18943
19896
  return errorResponse("Prompt is required when creating a Linear ticket", 400);
18944
19897
  }
18945
19898
  let resolvedBranch = branch;
19899
+ let resolvedPrompt = prompt;
19900
+ let resolvedMode = mode;
19901
+ if (body.fromLinear) {
19902
+ if (createLinearTicket) {
19903
+ return errorResponse("fromLinear cannot be combined with createLinearTicket", 400);
19904
+ }
19905
+ let conversationContext = body.fromLinear.conversationContext?.trim() ?? "";
19906
+ let seedBranch = null;
19907
+ if (!conversationContext || !resolvedBranch) {
19908
+ const seedResult = await buildSeedFromLinear({ issueId: body.fromLinear.issueId }, defaultSeedFromLinearDeps);
19909
+ if (!seedResult.ok) {
19910
+ return errorResponse(`Linear seed lookup failed: ${seedResult.error}`, seedResult.status);
19911
+ }
19912
+ if (!conversationContext && seedResult.data.conversationMarkdown) {
19913
+ conversationContext = seedResult.data.conversationMarkdown;
19914
+ }
19915
+ seedBranch = seedResult.data.branch;
19916
+ if (!resolvedBranch && seedBranch) {
19917
+ resolvedBranch = seedBranch;
19918
+ if (seedResult.data.source !== "none")
19919
+ resolvedMode = "existing";
19920
+ }
19921
+ }
19922
+ if (conversationContext) {
19923
+ resolvedPrompt = resolvedPrompt ? `${conversationContext}
19924
+
19925
+ ---
19926
+
19927
+ ${resolvedPrompt}` : conversationContext;
19928
+ }
19929
+ }
18946
19930
  if (createLinearTicket) {
18947
19931
  const title = deriveLinearIssueTitle(linearTitle, prompt);
18948
19932
  if (!title) {
@@ -18954,7 +19938,7 @@ async function apiCreateWorktree(req) {
18954
19938
  }
18955
19939
  const linearResult = await createLinearIssue({
18956
19940
  title,
18957
- description: prompt ?? "",
19941
+ description: resolvedPrompt ?? "",
18958
19942
  teamId
18959
19943
  });
18960
19944
  if (!linearResult.ok) {
@@ -18972,15 +19956,18 @@ async function apiCreateWorktree(req) {
18972
19956
  return errorResponse("Base branch must differ from branch name", 400);
18973
19957
  }
18974
19958
  }
18975
- log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${baseBranch ? ` base=${baseBranch}` : ""}${profile ? ` profile=${profile}` : ""} agents=${selectedAgents.join(",")}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
19959
+ const oneshot = normalizeOneshotConfig(body.oneshot);
19960
+ log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${baseBranch ? ` base=${baseBranch}` : ""}${profile ? ` profile=${profile}` : ""} agents=${selectedAgents.join(",")}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}${oneshot ? " oneshot=armed" : ""}`);
18976
19961
  const result = await lifecycleService.createWorktrees({
18977
- mode,
19962
+ mode: resolvedMode,
18978
19963
  branch: resolvedBranch,
18979
19964
  baseBranch,
18980
- prompt,
19965
+ prompt: resolvedPrompt,
18981
19966
  profile,
18982
19967
  ...agents && agents.length > 0 ? { agents } : { agent },
18983
- envOverrides
19968
+ envOverrides,
19969
+ ...body.source ? { source: body.source } : {},
19970
+ ...oneshot ? { oneshot } : {}
18984
19971
  });
18985
19972
  log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
18986
19973
  return jsonResponse({
@@ -18996,16 +19983,22 @@ async function apiDeleteWorktree(name) {
18996
19983
  return jsonResponse({ ok: true });
18997
19984
  });
18998
19985
  }
18999
- async function apiOpenWorktree(name) {
19986
+ async function apiOpenWorktree(name, req) {
19000
19987
  ensureBranchNotBusy(name);
19001
- log.info(`[worktree:open] name=${name}`);
19002
- const result = await lifecycleService.openWorktree(name);
19988
+ const parsed = await parseJsonBody(req, OpenWorktreeRequestSchema);
19989
+ if (!parsed.ok)
19990
+ return parsed.response;
19991
+ const prompt = parsed.data.prompt?.trim() ? parsed.data.prompt.trim() : undefined;
19992
+ const oneshot = normalizeOneshotConfig(parsed.data.oneshot);
19993
+ log.info(`[worktree:open] name=${name}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}${oneshot ? " oneshot=armed" : ""}`);
19994
+ const result = await lifecycleService.openWorktree(name, { prompt, ...oneshot ? { oneshot } : {} });
19003
19995
  log.debug(`[worktree:open] done name=${name} worktreeId=${result.worktreeId}`);
19004
19996
  return jsonResponse({ ok: true });
19005
19997
  }
19006
19998
  async function apiCloseWorktree(name) {
19007
19999
  ensureBranchNotBusy(name);
19008
20000
  log.info(`[worktree:close] name=${name}`);
20001
+ await disarmOneshotIfArmed(name, "close-worktree");
19009
20002
  await lifecycleService.closeWorktree(name);
19010
20003
  log.debug(`[worktree:close] done name=${name}`);
19011
20004
  return jsonResponse({ ok: true });
@@ -19017,6 +20010,7 @@ async function apiSetWorktreeArchived(name, req) {
19017
20010
  return parsed.response;
19018
20011
  const body = parsed.data;
19019
20012
  log.info(`[worktree:archive] name=${name} archived=${body.archived}`);
20013
+ await disarmOneshotIfArmed(name, "archive-worktree");
19020
20014
  await lifecycleService.setWorktreeArchived(name, body.archived);
19021
20015
  log.debug(`[worktree:archive] done name=${name} archived=${body.archived}`);
19022
20016
  return jsonResponse({ ok: true, archived: body.archived });
@@ -19041,6 +20035,7 @@ async function apiSendPrompt(name, req) {
19041
20035
  const text = body.text;
19042
20036
  const preamble = body.preamble;
19043
20037
  log.info(`[worktree:send] name=${name} text="${text.slice(0, 80)}"`);
20038
+ await disarmOneshotIfArmed(name, "send-prompt");
19044
20039
  const terminalWorktree = await resolveTerminalWorktree(name);
19045
20040
  const submitDelayMs = resolveWorktreeTerminalSubmitDelayMs(terminalWorktree.agentName);
19046
20041
  const result = await sendPrompt(terminalWorktree.worktreeId, terminalWorktree.attachTarget, text, 0, preamble, submitDelayMs);
@@ -19051,6 +20046,7 @@ async function apiSendPrompt(name, req) {
19051
20046
  async function apiMergeWorktree(name) {
19052
20047
  ensureBranchNotBusy(name);
19053
20048
  log.info(`[worktree:merge] name=${name}`);
20049
+ await disarmOneshotIfArmed(name, "merge-worktree");
19054
20050
  await lifecycleService.mergeWorktree(name);
19055
20051
  log.debug(`[worktree:merge] done name=${name}`);
19056
20052
  return jsonResponse({ ok: true });
@@ -19174,6 +20170,74 @@ async function apiPullMain(req) {
19174
20170
  log.info(`[pull-main] ${repo || "main"} ${force ? "force " : ""}pull: ${result.status}`);
19175
20171
  return jsonResponse(result);
19176
20172
  }
20173
+ async function postWorktreeConversationToLinear(branch, target) {
20174
+ if (!config.integrations.linear.enabled) {
20175
+ return { ok: false, error: "Linear integration is disabled", status: 400 };
20176
+ }
20177
+ const apiKey = Bun.env.LINEAR_API_KEY;
20178
+ if (!apiKey?.trim()) {
20179
+ return { ok: false, error: "LINEAR_API_KEY not set", status: 503 };
20180
+ }
20181
+ await reconciliationService.reconcile(PROJECT_DIR);
20182
+ const state = projectRuntime.getWorktreeByBranch(branch);
20183
+ if (!state)
20184
+ return { ok: false, error: `Worktree not found: ${branch}`, status: 404 };
20185
+ const resolved = await resolveAgentsWorktree(branch);
20186
+ if (!resolved.ok) {
20187
+ return { ok: false, error: `Worktree not found: ${branch}`, status: 404 };
20188
+ }
20189
+ const chatSupport = resolveWorktreeAgentChatSupport(resolved.worktree, "chat");
20190
+ if (!chatSupport.ok)
20191
+ return { ok: false, error: chatSupport.error, status: chatSupport.status };
20192
+ const conversationResult = chatSupport.data.provider === "claude" ? await claudeConversationService.readWorktreeConversation(resolved.worktree) : await worktreeConversationService.readWorktreeConversation(resolved.worktree);
20193
+ if (!conversationResult.ok)
20194
+ return { ok: false, error: conversationResult.error, status: conversationResult.status };
20195
+ const prUrl = (state.prs ?? []).find((pr) => pr.state === "open" || pr.state === "merged")?.url ?? null;
20196
+ const exportInput = {
20197
+ target,
20198
+ branch,
20199
+ baseBranch: state.baseBranch ?? null,
20200
+ agent: resolved.worktree.agentName ?? null,
20201
+ prUrl,
20202
+ conversation: conversationResult.data.conversation,
20203
+ webmuxVersion: package_default.version
20204
+ };
20205
+ const deps = {
20206
+ fetchIssueWithAttachments,
20207
+ fetchTeamByKey,
20208
+ createLinearIssue,
20209
+ uploadAttachmentFile,
20210
+ attachToIssue,
20211
+ createIssueComment
20212
+ };
20213
+ const result = await exportConversationToLinear(exportInput, deps);
20214
+ if (!result.ok)
20215
+ return { ok: false, error: result.error, status: result.status };
20216
+ return { ok: true, data: result.data };
20217
+ }
20218
+ async function apiPostWorktreeToLinear(name, req) {
20219
+ const parsed = await parseJsonBody(req, PostWorktreeToLinearRequestSchema);
20220
+ if (!parsed.ok)
20221
+ return parsed.response;
20222
+ const outcome = await postWorktreeConversationToLinear(name, parsed.data.target);
20223
+ if (!outcome.ok)
20224
+ return errorResponse(outcome.error, outcome.status);
20225
+ return jsonResponse({
20226
+ ok: true,
20227
+ issueId: outcome.data.issueId,
20228
+ issueUrl: outcome.data.issueUrl,
20229
+ commentUrl: outcome.data.commentUrl,
20230
+ attachmentUrl: outcome.data.attachmentUrl
20231
+ });
20232
+ }
20233
+ async function apiSyncWorktreePrs(name) {
20234
+ await syncPrStatus(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJECT_DIR);
20235
+ const snapshot = await readProjectSnapshot();
20236
+ const worktree = snapshot.worktrees.find((w) => w.branch === name);
20237
+ if (!worktree)
20238
+ return errorResponse(`Worktree not found: ${name}`, 404);
20239
+ return jsonResponse(worktree);
20240
+ }
19177
20241
  async function apiGetLinearIssues() {
19178
20242
  const apiKey = Bun.env.LINEAR_API_KEY;
19179
20243
  const fetchResult = config.integrations.linear.enabled && apiKey?.trim() ? await fetchAssignedIssues() : undefined;
@@ -19231,6 +20295,7 @@ async function apiUploadFiles(name, req) {
19231
20295
  const state = projectRuntime.getWorktreeByBranch(name);
19232
20296
  if (!state)
19233
20297
  return errorResponse(`Worktree not found: ${name}`, 404);
20298
+ await disarmOneshotIfArmed(name, "upload-files");
19234
20299
  let formData;
19235
20300
  try {
19236
20301
  formData = await req.formData();
@@ -19253,7 +20318,7 @@ async function apiUploadFiles(name, req) {
19253
20318
  return errorResponse(`File too large: ${entry.name} (max 10MB)`, 400);
19254
20319
  }
19255
20320
  const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
19256
- const destPath = join8(uploadDir, safeName);
20321
+ const destPath = join7(uploadDir, safeName);
19257
20322
  if (!resolve9(destPath).startsWith(uploadDir + "/")) {
19258
20323
  return errorResponse("Invalid filename", 400);
19259
20324
  }
@@ -19417,7 +20482,7 @@ Bun.serve({
19417
20482
  if (!parsed.ok)
19418
20483
  return parsed.response;
19419
20484
  const name = parsed.data;
19420
- return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name));
20485
+ return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name, req));
19421
20486
  }
19422
20487
  },
19423
20488
  "/api/worktrees/:name/terminal-launch": {
@@ -19447,6 +20512,24 @@ Bun.serve({
19447
20512
  return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
19448
20513
  }
19449
20514
  },
20515
+ [apiPaths.postWorktreeToLinear]: {
20516
+ POST: (req) => {
20517
+ const parsed = parseWorktreeNameParam(req.params);
20518
+ if (!parsed.ok)
20519
+ return parsed.response;
20520
+ const name = parsed.data;
20521
+ return catching(`POST /api/worktrees/${name}/linear/post`, () => apiPostWorktreeToLinear(name, req));
20522
+ }
20523
+ },
20524
+ [apiPaths.syncWorktreePrs]: {
20525
+ POST: (req) => {
20526
+ const parsed = parseWorktreeNameParam(req.params);
20527
+ if (!parsed.ok)
20528
+ return parsed.response;
20529
+ const name = parsed.data;
20530
+ return catching(`POST /api/worktrees/${name}/sync-prs`, () => apiSyncWorktreePrs(name));
20531
+ }
20532
+ },
19450
20533
  [apiPaths.setWorktreeLabel]: {
19451
20534
  PUT: (req) => {
19452
20535
  const parsed = parseWorktreeNameParam(req.params);
@@ -19531,7 +20614,7 @@ Bun.serve({
19531
20614
  const url = new URL(req.url);
19532
20615
  if (STATIC_DIR) {
19533
20616
  const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
19534
- const filePath = join8(STATIC_DIR, rawPath);
20617
+ const filePath = join7(STATIC_DIR, rawPath);
19535
20618
  const staticRoot = resolve9(STATIC_DIR);
19536
20619
  if (!resolve9(filePath).startsWith(staticRoot + "/")) {
19537
20620
  return new Response("Forbidden", { status: 403 });
@@ -19541,7 +20624,7 @@ Bun.serve({
19541
20624
  const headers = rawPath.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : {};
19542
20625
  return new Response(file, { headers });
19543
20626
  }
19544
- return new Response(Bun.file(join8(STATIC_DIR, "index.html")), {
20627
+ return new Response(Bun.file(join7(STATIC_DIR, "index.html")), {
19545
20628
  headers: { "Cache-Control": "no-cache" }
19546
20629
  });
19547
20630
  }
@@ -19577,6 +20660,9 @@ Bun.serve({
19577
20660
  const attachId = getAttachedSessionId(data, ws);
19578
20661
  if (!attachId)
19579
20662
  return;
20663
+ if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20664
+ disarmOneshotIfArmed(branch, "terminal-ws-input");
20665
+ }
19580
20666
  write(attachId, msg.data);
19581
20667
  break;
19582
20668
  }
@@ -19584,6 +20670,9 @@ Bun.serve({
19584
20670
  const attachId = getAttachedSessionId(data, ws);
19585
20671
  if (!attachId)
19586
20672
  return;
20673
+ if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20674
+ disarmOneshotIfArmed(branch, "terminal-ws-send-keys");
20675
+ }
19587
20676
  await sendKeys(attachId, msg.hexBytes);
19588
20677
  break;
19589
20678
  }
@@ -19664,6 +20753,15 @@ startPrMonitor(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJE
19664
20753
  if (linearAutoCreateEnabled) {
19665
20754
  startLinearAutoCreate();
19666
20755
  }
20756
+ startOneshotWatcher({
20757
+ projectRuntime,
20758
+ lifecycleService,
20759
+ postToLinear: async (branch, target) => {
20760
+ const outcome = await postWorktreeConversationToLinear(branch, target);
20761
+ if (!outcome.ok)
20762
+ throw new Error(outcome.error);
20763
+ }
20764
+ });
19667
20765
  if (config.workspace.autoPull.enabled) {
19668
20766
  startAutoPullMonitor({ git, projectRoot: PROJECT_DIR, mainBranch: config.workspace.mainBranch }, config.workspace.autoPull.intervalSeconds * 1000);
19669
20767
  }