webmux 0.31.2 → 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
  }
@@ -15353,7 +16051,9 @@ async function initializeManagedWorktree(opts) {
15353
16051
  agent: opts.agent,
15354
16052
  runtime: opts.runtime,
15355
16053
  startupEnvValues: { ...opts.startupEnvValues ?? {} },
15356
- allocatedPorts: { ...opts.allocatedPorts ?? {} }
16054
+ allocatedPorts: { ...opts.allocatedPorts ?? {} },
16055
+ ...opts.source ? { source: opts.source } : {},
16056
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
15357
16057
  };
15358
16058
  const paths = await ensureWorktreeStorageDirs(opts.gitDir);
15359
16059
  await writeWorktreeMeta(opts.gitDir, meta);
@@ -15406,7 +16106,9 @@ async function createManagedWorktree(opts, deps = {}) {
15406
16106
  controlUrl: opts.controlUrl,
15407
16107
  controlToken: opts.controlToken,
15408
16108
  now: opts.now,
15409
- worktreeId: opts.worktreeId
16109
+ worktreeId: opts.worktreeId,
16110
+ ...opts.source ? { source: opts.source } : {},
16111
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
15410
16112
  });
15411
16113
  if (deps.tmux) {
15412
16114
  sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
@@ -15555,10 +16257,15 @@ class LifecycleService {
15555
16257
  agent: agent.id
15556
16258
  });
15557
16259
  }
15558
- async openWorktree(branch) {
16260
+ async openWorktree(branch, options = {}) {
15559
16261
  try {
15560
16262
  const resolved = await this.resolveExistingWorktree(branch);
15561
- 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
+ }
15562
16269
  const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
15563
16270
  const agent = this.resolveAgentDefinition(initialized.meta.agent);
15564
16271
  const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
@@ -15573,7 +16280,8 @@ class LifecycleService {
15573
16280
  agent,
15574
16281
  initialized,
15575
16282
  worktreePath: resolved.entry.path,
15576
- launchMode
16283
+ launchMode,
16284
+ followUpPrompt: options.prompt
15577
16285
  });
15578
16286
  await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
15579
16287
  return {
@@ -15584,6 +16292,20 @@ class LifecycleService {
15584
16292
  throw this.wrapOperationError(error);
15585
16293
  }
15586
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
+ }
15587
16309
  async closeWorktree(branch) {
15588
16310
  try {
15589
16311
  await this.resolveExistingWorktree(branch);
@@ -15870,8 +16592,10 @@ class LifecycleService {
15870
16592
  agent: input.agent,
15871
16593
  initialized: input.initialized,
15872
16594
  worktreePath: input.worktreePath,
15873
- prompt: input.prompt,
16595
+ creationPrompt: input.creationPrompt,
16596
+ followUpPrompt: input.followUpPrompt,
15874
16597
  launchMode: input.launchMode,
16598
+ source: input.source,
15875
16599
  containerName
15876
16600
  }));
15877
16601
  return;
@@ -15883,12 +16607,19 @@ class LifecycleService {
15883
16607
  agent: input.agent,
15884
16608
  initialized: input.initialized,
15885
16609
  worktreePath: input.worktreePath,
15886
- prompt: input.prompt,
15887
- launchMode: input.launchMode
16610
+ creationPrompt: input.creationPrompt,
16611
+ followUpPrompt: input.followUpPrompt,
16612
+ launchMode: input.launchMode,
16613
+ source: input.source
15888
16614
  }));
15889
16615
  }
15890
16616
  buildSessionLayout(input) {
15891
- 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;
15892
16623
  const containerName = input.containerName;
15893
16624
  return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
15894
16625
  repoRoot: this.deps.projectRoot,
@@ -15903,7 +16634,7 @@ class LifecycleService {
15903
16634
  profileName: input.profileName,
15904
16635
  yolo: input.profile.yolo === true,
15905
16636
  systemPrompt,
15906
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
16637
+ prompt,
15907
16638
  launchMode: input.launchMode
15908
16639
  }),
15909
16640
  shell: buildDockerShellCommand(containerName, input.worktreePath, input.initialized.paths.runtimeEnvPath)
@@ -15917,7 +16648,7 @@ class LifecycleService {
15917
16648
  profileName: input.profileName,
15918
16649
  yolo: input.profile.yolo === true,
15919
16650
  systemPrompt,
15920
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
16651
+ prompt,
15921
16652
  launchMode: input.launchMode
15922
16653
  }),
15923
16654
  shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
@@ -16041,12 +16772,14 @@ class LifecycleService {
16041
16772
  const { profileName, profile } = this.resolveProfile(input.profile);
16042
16773
  const agent = this.resolveAgentDefinition(input.agent);
16043
16774
  const worktreePath = this.resolveWorktreePath(input.branch);
16775
+ const source = input.source ?? "ui";
16044
16776
  const createProgressBase = {
16045
16777
  branch: input.branch,
16046
16778
  ...baseBranch ? { baseBranch } : {},
16047
16779
  path: worktreePath,
16048
16780
  profile: profileName,
16049
- agent: input.agent
16781
+ agent: input.agent,
16782
+ source
16050
16783
  };
16051
16784
  const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
16052
16785
  let initialized = null;
@@ -16071,7 +16804,9 @@ class LifecycleService {
16071
16804
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
16072
16805
  controlUrl: this.controlUrl(profile.runtime),
16073
16806
  controlToken: await this.deps.getControlToken(),
16074
- deleteBranchOnRollback
16807
+ deleteBranchOnRollback,
16808
+ source,
16809
+ ...input.oneshot ? { oneshot: input.oneshot } : {}
16075
16810
  }, {
16076
16811
  git: this.deps.git
16077
16812
  });
@@ -16109,8 +16844,9 @@ class LifecycleService {
16109
16844
  agent,
16110
16845
  initialized,
16111
16846
  worktreePath,
16112
- prompt: input.prompt,
16113
- launchMode: "fresh"
16847
+ creationPrompt: input.prompt,
16848
+ launchMode: "fresh",
16849
+ source
16114
16850
  });
16115
16851
  await this.reportCreateProgress({
16116
16852
  ...createProgressBase,
@@ -16596,17 +17332,27 @@ function startPrMonitor(getWorktreeGitDirs, linkedRepos, projectDir, intervalMs
16596
17332
  var LINEAR_AUTO_CREATE_POLL_INTERVAL_MS = 60000;
16597
17333
  var processedIssueIds = new Set;
16598
17334
  var AUTO_CREATE_LABEL = "webmux";
16599
- 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) {
16600
17340
  return issues.filter((issue) => {
16601
17341
  if (issue.state.name !== "Todo")
16602
17342
  return false;
16603
- if (!issue.labels.some((l) => l.name.toLowerCase() === AUTO_CREATE_LABEL))
17343
+ if (!matchesLabelRule(issue))
16604
17344
  return false;
16605
17345
  if (processedIssueIds.has(issue.id))
16606
17346
  return false;
16607
17347
  return !existingBranches.some((branch) => branchMatchesIssue(branch, issue.branchName));
16608
17348
  });
16609
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
+ }
16610
17356
  async function runLinearAutoCreateOnce(deps) {
16611
17357
  const fetchIssues = deps.fetchIssues ?? fetchAssignedIssues;
16612
17358
  const result = await fetchIssues({ skipCache: true });
@@ -16614,29 +17360,53 @@ async function runLinearAutoCreateOnce(deps) {
16614
17360
  log.error(`[linear-auto-create] failed to fetch issues: ${result.error}`);
16615
17361
  return;
16616
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
+ }
16617
17368
  const projectRoot2 = deps.projectRoot;
16618
17369
  const existingBranches = deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch);
16619
- const newIssues = filterAutoCreateIssues(result.data, existingBranches);
16620
- 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) {
16621
17373
  log.debug(`[linear-auto-create] no new labeled issues (${result.data.length} assigned, ${existingBranches.length} worktrees)`);
16622
17374
  return;
16623
17375
  }
16624
- log.info(`[linear-auto-create] found ${newIssues.length} new issue(s) with "${AUTO_CREATE_LABEL}" label`);
16625
- for (const issue of newIssues) {
16626
- try {
16627
- log.info(`[linear-auto-create] creating worktree for ${issue.identifier}: ${issue.title}`);
16628
- await deps.lifecycleService.createWorktree({
16629
- mode: "new",
16630
- branch: issue.branchName,
16631
- 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}
16632
17400
 
16633
17401
  ${issue.description ?? ""}`.trim()
16634
- });
16635
- processedIssueIds.add(issue.id);
16636
- log.info(`[linear-auto-create] created worktree for ${issue.identifier}`);
16637
- } catch (err) {
16638
- const msg = err instanceof Error ? err.message : String(err);
16639
- 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
+ }
16640
17410
  }
16641
17411
  }
16642
17412
  }
@@ -16648,6 +17418,90 @@ function resetProcessedIssues() {
16648
17418
  processedIssueIds.clear();
16649
17419
  }
16650
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
+
16651
17505
  // backend/src/services/auto-remove-service.ts
16652
17506
  async function runAutoRemove(deps) {
16653
17507
  const worktrees = deps.git.listLiveWorktrees(deps.projectRoot).filter((e) => !e.bare && e.branch !== null && e.path !== deps.projectRoot);
@@ -16860,7 +17714,9 @@ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue,
16860
17714
  services: state.services.map((service) => ({ ...service })),
16861
17715
  prs: state.prs.map((pr) => clonePrEntry(pr)),
16862
17716
  linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
16863
- creation: mapCreationSnapshot(creating)
17717
+ creation: mapCreationSnapshot(creating),
17718
+ source: state.source,
17719
+ oneshot: state.oneshot
16864
17720
  };
16865
17721
  }
16866
17722
  function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
@@ -16883,7 +17739,9 @@ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, find
16883
17739
  services: [],
16884
17740
  prs: [],
16885
17741
  linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
16886
- creation: mapCreationSnapshot(creating)
17742
+ creation: mapCreationSnapshot(creating),
17743
+ source: creating.source,
17744
+ oneshot: null
16887
17745
  };
16888
17746
  }
16889
17747
  function buildWorktreeSnapshots(input) {
@@ -17041,13 +17899,18 @@ class ClaudeConversationService {
17041
17899
  }
17042
17900
  async resolveSession(meta, cwd) {
17043
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
+ }
17044
17908
  if (savedSessionId) {
17045
17909
  const savedSession = await this.deps.claude.readSession(savedSessionId, cwd);
17046
17910
  if (savedSession)
17047
17911
  return savedSession;
17048
17912
  log.warn(`[agents] saved Claude session missing, rediscovering cwd=${cwd} sessionId=${savedSessionId}`);
17049
17913
  }
17050
- const discovered = (await this.deps.claude.listSessions(cwd))[0] ?? null;
17051
17914
  if (!discovered)
17052
17915
  return null;
17053
17916
  return await this.deps.claude.readSession(discovered.sessionId, cwd);
@@ -17561,7 +18424,7 @@ async function removeContainer(branch) {
17561
18424
  }
17562
18425
 
17563
18426
  // backend/src/adapters/hooks.ts
17564
- import { join as join7 } from "path";
18427
+ import { join as join6 } from "path";
17565
18428
  function buildErrorMessage(name, exitCode, stdout, stderr) {
17566
18429
  const output = stderr.trim() || stdout.trim();
17567
18430
  if (output) {
@@ -17586,7 +18449,7 @@ class BunLifecycleHookRunner {
17586
18449
  return this.direnvAvailable;
17587
18450
  }
17588
18451
  async buildCommand(cwd, command) {
17589
- if (this.checkDirenv() && await Bun.file(join7(cwd, ".envrc")).exists()) {
18452
+ if (this.checkDirenv() && await Bun.file(join6(cwd, ".envrc")).exists()) {
17590
18453
  Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
17591
18454
  return ["direnv", "exec", cwd, "bash", "-c", command];
17592
18455
  }
@@ -18006,6 +18869,8 @@ function makeDefaultState(input) {
18006
18869
  path: input.path,
18007
18870
  profile: input.profile ?? null,
18008
18871
  agentName: input.agentName ?? null,
18872
+ source: input.source ?? "ui",
18873
+ oneshot: input.oneshot ?? null,
18009
18874
  git: {
18010
18875
  exists: true,
18011
18876
  branch: input.branch,
@@ -18055,6 +18920,10 @@ class ProjectRuntime {
18055
18920
  existing.agentName = input.agentName ?? existing.agentName;
18056
18921
  if (input.runtime)
18057
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;
18058
18927
  existing.git.exists = true;
18059
18928
  existing.git.branch = input.branch;
18060
18929
  existing.session.windowName = buildWorktreeWindowName(input.branch);
@@ -18065,6 +18934,11 @@ class ProjectRuntime {
18065
18934
  this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
18066
18935
  return created;
18067
18936
  }
18937
+ setOneshot(worktreeId, oneshot) {
18938
+ const state = this.requireWorktree(worktreeId);
18939
+ state.oneshot = oneshot;
18940
+ return state;
18941
+ }
18068
18942
  removeWorktree(worktreeId) {
18069
18943
  const state = this.worktrees.get(worktreeId);
18070
18944
  if (!state)
@@ -18258,6 +19132,8 @@ class ReconciliationService {
18258
19132
  profile: meta?.profile ?? null,
18259
19133
  agentName: meta?.agent ?? null,
18260
19134
  runtime: meta?.runtime ?? "host",
19135
+ source: meta?.source ?? "ui",
19136
+ oneshot: meta?.oneshot ?? null,
18261
19137
  git: {
18262
19138
  dirty: gitStatus.dirty,
18263
19139
  aheadCount: gitStatus.aheadCount,
@@ -18290,7 +19166,9 @@ class ReconciliationService {
18290
19166
  path: state.path,
18291
19167
  profile: state.profile,
18292
19168
  agentName: state.agentName,
18293
- runtime: state.runtime
19169
+ runtime: state.runtime,
19170
+ source: state.source,
19171
+ oneshot: state.oneshot
18294
19172
  });
18295
19173
  this.deps.runtime.setGitState(state.worktreeId, {
18296
19174
  exists: true,
@@ -18325,7 +19203,8 @@ class WorktreeCreationTracker {
18325
19203
  path: progress.path,
18326
19204
  profile: progress.profile,
18327
19205
  agentName: progress.agent,
18328
- phase: progress.phase
19206
+ phase: progress.phase,
19207
+ source: progress.source
18329
19208
  };
18330
19209
  this.worktrees.set(progress.branch, next);
18331
19210
  }
@@ -18435,15 +19314,60 @@ var lifecycleService = runtime.lifecycleService;
18435
19314
  var linearAutoCreateEnabled = config.integrations.linear.autoCreateWorktrees;
18436
19315
  var stopLinearAutoCreate = null;
18437
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
+ }
18438
19339
  function startLinearAutoCreate() {
18439
19340
  if (stopLinearAutoCreate)
18440
19341
  return;
18441
19342
  stopLinearAutoCreate = startLinearAutoCreateMonitor({
18442
19343
  lifecycleService,
18443
19344
  git,
18444
- projectRoot: PROJECT_DIR
19345
+ projectRoot: PROJECT_DIR,
19346
+ runOneshotForIssue
18445
19347
  });
18446
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
+ }
18447
19371
  function stopLinearAutoCreateMonitor() {
18448
19372
  if (stopLinearAutoCreate) {
18449
19373
  stopLinearAutoCreate();
@@ -18753,6 +19677,7 @@ async function apiGetAgentsWorktreeHistory(branch) {
18753
19677
  }
18754
19678
  async function apiSendAgentsWorktreeMessage(branch, req) {
18755
19679
  touchDashboardActivity();
19680
+ await disarmOneshotIfArmed(branch, "agents-send-message");
18756
19681
  const parsed = await parseJsonBody(req, AgentsSendMessageRequestSchema);
18757
19682
  if (!parsed.ok)
18758
19683
  return parsed.response;
@@ -18785,6 +19710,7 @@ async function apiSendAgentsWorktreeMessage(branch, req) {
18785
19710
  }
18786
19711
  async function apiInterruptAgentsWorktree(branch) {
18787
19712
  touchDashboardActivity();
19713
+ await disarmOneshotIfArmed(branch, "agents-interrupt");
18788
19714
  const resolved = await resolveAgentsWorktree(branch);
18789
19715
  if (!resolved.ok)
18790
19716
  return resolved.response;
@@ -18970,6 +19896,37 @@ async function apiCreateWorktree(req) {
18970
19896
  return errorResponse("Prompt is required when creating a Linear ticket", 400);
18971
19897
  }
18972
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
+ }
18973
19930
  if (createLinearTicket) {
18974
19931
  const title = deriveLinearIssueTitle(linearTitle, prompt);
18975
19932
  if (!title) {
@@ -18981,7 +19938,7 @@ async function apiCreateWorktree(req) {
18981
19938
  }
18982
19939
  const linearResult = await createLinearIssue({
18983
19940
  title,
18984
- description: prompt ?? "",
19941
+ description: resolvedPrompt ?? "",
18985
19942
  teamId
18986
19943
  });
18987
19944
  if (!linearResult.ok) {
@@ -18999,15 +19956,18 @@ async function apiCreateWorktree(req) {
18999
19956
  return errorResponse("Base branch must differ from branch name", 400);
19000
19957
  }
19001
19958
  }
19002
- 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" : ""}`);
19003
19961
  const result = await lifecycleService.createWorktrees({
19004
- mode,
19962
+ mode: resolvedMode,
19005
19963
  branch: resolvedBranch,
19006
19964
  baseBranch,
19007
- prompt,
19965
+ prompt: resolvedPrompt,
19008
19966
  profile,
19009
19967
  ...agents && agents.length > 0 ? { agents } : { agent },
19010
- envOverrides
19968
+ envOverrides,
19969
+ ...body.source ? { source: body.source } : {},
19970
+ ...oneshot ? { oneshot } : {}
19011
19971
  });
19012
19972
  log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
19013
19973
  return jsonResponse({
@@ -19023,16 +19983,22 @@ async function apiDeleteWorktree(name) {
19023
19983
  return jsonResponse({ ok: true });
19024
19984
  });
19025
19985
  }
19026
- async function apiOpenWorktree(name) {
19986
+ async function apiOpenWorktree(name, req) {
19027
19987
  ensureBranchNotBusy(name);
19028
- log.info(`[worktree:open] name=${name}`);
19029
- 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 } : {} });
19030
19995
  log.debug(`[worktree:open] done name=${name} worktreeId=${result.worktreeId}`);
19031
19996
  return jsonResponse({ ok: true });
19032
19997
  }
19033
19998
  async function apiCloseWorktree(name) {
19034
19999
  ensureBranchNotBusy(name);
19035
20000
  log.info(`[worktree:close] name=${name}`);
20001
+ await disarmOneshotIfArmed(name, "close-worktree");
19036
20002
  await lifecycleService.closeWorktree(name);
19037
20003
  log.debug(`[worktree:close] done name=${name}`);
19038
20004
  return jsonResponse({ ok: true });
@@ -19044,6 +20010,7 @@ async function apiSetWorktreeArchived(name, req) {
19044
20010
  return parsed.response;
19045
20011
  const body = parsed.data;
19046
20012
  log.info(`[worktree:archive] name=${name} archived=${body.archived}`);
20013
+ await disarmOneshotIfArmed(name, "archive-worktree");
19047
20014
  await lifecycleService.setWorktreeArchived(name, body.archived);
19048
20015
  log.debug(`[worktree:archive] done name=${name} archived=${body.archived}`);
19049
20016
  return jsonResponse({ ok: true, archived: body.archived });
@@ -19068,6 +20035,7 @@ async function apiSendPrompt(name, req) {
19068
20035
  const text = body.text;
19069
20036
  const preamble = body.preamble;
19070
20037
  log.info(`[worktree:send] name=${name} text="${text.slice(0, 80)}"`);
20038
+ await disarmOneshotIfArmed(name, "send-prompt");
19071
20039
  const terminalWorktree = await resolveTerminalWorktree(name);
19072
20040
  const submitDelayMs = resolveWorktreeTerminalSubmitDelayMs(terminalWorktree.agentName);
19073
20041
  const result = await sendPrompt(terminalWorktree.worktreeId, terminalWorktree.attachTarget, text, 0, preamble, submitDelayMs);
@@ -19078,6 +20046,7 @@ async function apiSendPrompt(name, req) {
19078
20046
  async function apiMergeWorktree(name) {
19079
20047
  ensureBranchNotBusy(name);
19080
20048
  log.info(`[worktree:merge] name=${name}`);
20049
+ await disarmOneshotIfArmed(name, "merge-worktree");
19081
20050
  await lifecycleService.mergeWorktree(name);
19082
20051
  log.debug(`[worktree:merge] done name=${name}`);
19083
20052
  return jsonResponse({ ok: true });
@@ -19201,6 +20170,74 @@ async function apiPullMain(req) {
19201
20170
  log.info(`[pull-main] ${repo || "main"} ${force ? "force " : ""}pull: ${result.status}`);
19202
20171
  return jsonResponse(result);
19203
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
+ }
19204
20241
  async function apiGetLinearIssues() {
19205
20242
  const apiKey = Bun.env.LINEAR_API_KEY;
19206
20243
  const fetchResult = config.integrations.linear.enabled && apiKey?.trim() ? await fetchAssignedIssues() : undefined;
@@ -19258,6 +20295,7 @@ async function apiUploadFiles(name, req) {
19258
20295
  const state = projectRuntime.getWorktreeByBranch(name);
19259
20296
  if (!state)
19260
20297
  return errorResponse(`Worktree not found: ${name}`, 404);
20298
+ await disarmOneshotIfArmed(name, "upload-files");
19261
20299
  let formData;
19262
20300
  try {
19263
20301
  formData = await req.formData();
@@ -19280,7 +20318,7 @@ async function apiUploadFiles(name, req) {
19280
20318
  return errorResponse(`File too large: ${entry.name} (max 10MB)`, 400);
19281
20319
  }
19282
20320
  const safeName = `${Date.now()}_${sanitizeFilename(entry.name)}`;
19283
- const destPath = join8(uploadDir, safeName);
20321
+ const destPath = join7(uploadDir, safeName);
19284
20322
  if (!resolve9(destPath).startsWith(uploadDir + "/")) {
19285
20323
  return errorResponse("Invalid filename", 400);
19286
20324
  }
@@ -19444,7 +20482,7 @@ Bun.serve({
19444
20482
  if (!parsed.ok)
19445
20483
  return parsed.response;
19446
20484
  const name = parsed.data;
19447
- return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name));
20485
+ return catching(`POST /api/worktrees/${name}/open`, () => apiOpenWorktree(name, req));
19448
20486
  }
19449
20487
  },
19450
20488
  "/api/worktrees/:name/terminal-launch": {
@@ -19474,6 +20512,24 @@ Bun.serve({
19474
20512
  return catching(`PUT /api/worktrees/${name}/archive`, () => apiSetWorktreeArchived(name, req));
19475
20513
  }
19476
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
+ },
19477
20533
  [apiPaths.setWorktreeLabel]: {
19478
20534
  PUT: (req) => {
19479
20535
  const parsed = parseWorktreeNameParam(req.params);
@@ -19558,7 +20614,7 @@ Bun.serve({
19558
20614
  const url = new URL(req.url);
19559
20615
  if (STATIC_DIR) {
19560
20616
  const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
19561
- const filePath = join8(STATIC_DIR, rawPath);
20617
+ const filePath = join7(STATIC_DIR, rawPath);
19562
20618
  const staticRoot = resolve9(STATIC_DIR);
19563
20619
  if (!resolve9(filePath).startsWith(staticRoot + "/")) {
19564
20620
  return new Response("Forbidden", { status: 403 });
@@ -19568,7 +20624,7 @@ Bun.serve({
19568
20624
  const headers = rawPath.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : {};
19569
20625
  return new Response(file, { headers });
19570
20626
  }
19571
- return new Response(Bun.file(join8(STATIC_DIR, "index.html")), {
20627
+ return new Response(Bun.file(join7(STATIC_DIR, "index.html")), {
19572
20628
  headers: { "Cache-Control": "no-cache" }
19573
20629
  });
19574
20630
  }
@@ -19604,6 +20660,9 @@ Bun.serve({
19604
20660
  const attachId = getAttachedSessionId(data, ws);
19605
20661
  if (!attachId)
19606
20662
  return;
20663
+ if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20664
+ disarmOneshotIfArmed(branch, "terminal-ws-input");
20665
+ }
19607
20666
  write(attachId, msg.data);
19608
20667
  break;
19609
20668
  }
@@ -19611,6 +20670,9 @@ Bun.serve({
19611
20670
  const attachId = getAttachedSessionId(data, ws);
19612
20671
  if (!attachId)
19613
20672
  return;
20673
+ if (projectRuntime.getWorktreeByBranch(branch)?.oneshot) {
20674
+ disarmOneshotIfArmed(branch, "terminal-ws-send-keys");
20675
+ }
19614
20676
  await sendKeys(attachId, msg.hexBytes);
19615
20677
  break;
19616
20678
  }
@@ -19691,6 +20753,15 @@ startPrMonitor(getWorktreeGitDirs, config.integrations.github.linkedRepos, PROJE
19691
20753
  if (linearAutoCreateEnabled) {
19692
20754
  startLinearAutoCreate();
19693
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
+ });
19694
20765
  if (config.workspace.autoPull.enabled) {
19695
20766
  startAutoPullMonitor({ git, projectRoot: PROJECT_DIR, mainBranch: config.workspace.mainBranch }, config.workspace.autoPull.intervalSeconds * 1000);
19696
20767
  }