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.
package/bin/webmux.js CHANGED
@@ -50,12 +50,26 @@ var __require = import.meta.require;
50
50
  // backend/src/adapters/git.ts
51
51
  import { readdirSync, rmSync, statSync } from "fs";
52
52
  import { resolve, join } from "path";
53
+ function spawnGit(args, cwd) {
54
+ try {
55
+ return {
56
+ ok: true,
57
+ result: Bun.spawnSync(["git", ...args], {
58
+ cwd,
59
+ stdout: "pipe",
60
+ stderr: "pipe"
61
+ })
62
+ };
63
+ } catch (error) {
64
+ return { ok: false, stderr: `spawn error (cwd=${cwd}): ${errorMessage(error)}` };
65
+ }
66
+ }
53
67
  function runGit(args, cwd) {
54
- const result = Bun.spawnSync(["git", ...args], {
55
- cwd,
56
- stdout: "pipe",
57
- stderr: "pipe"
58
- });
68
+ const spawned = spawnGit(args, cwd);
69
+ if (!spawned.ok) {
70
+ throw new Error(`git ${args.join(" ")} failed: ${spawned.stderr}`);
71
+ }
72
+ const { result } = spawned;
59
73
  if (result.exitCode !== 0) {
60
74
  const stderr = new TextDecoder().decode(result.stderr).trim();
61
75
  throw new Error(`git ${args.join(" ")} failed: ${stderr || `exit ${result.exitCode}`}`);
@@ -63,11 +77,11 @@ function runGit(args, cwd) {
63
77
  return new TextDecoder().decode(result.stdout).trim();
64
78
  }
65
79
  function tryRunGit(args, cwd) {
66
- const result = Bun.spawnSync(["git", ...args], {
67
- cwd,
68
- stdout: "pipe",
69
- stderr: "pipe"
70
- });
80
+ const spawned = spawnGit(args, cwd);
81
+ if (!spawned.ok) {
82
+ return { ok: false, stderr: spawned.stderr };
83
+ }
84
+ const { result } = spawned;
71
85
  if (result.exitCode !== 0) {
72
86
  return {
73
87
  ok: false,
@@ -188,6 +202,16 @@ function listGitWorktrees(cwd) {
188
202
  const output = runGit(["worktree", "list", "--porcelain"], cwd);
189
203
  return parseGitWorktreePorcelain(output);
190
204
  }
205
+ function worktreeEntryPathExists(entry) {
206
+ try {
207
+ return statSync(entry.path).isDirectory();
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+ function filterLiveWorktreeEntries(entries) {
213
+ return entries.filter(worktreeEntryPathExists);
214
+ }
191
215
  function listLocalGitBranches(cwd) {
192
216
  const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd);
193
217
  return output.split(`
@@ -248,6 +272,9 @@ class BunGitGateway {
248
272
  listWorktrees(cwd) {
249
273
  return listGitWorktrees(cwd);
250
274
  }
275
+ listLiveWorktrees(cwd) {
276
+ return filterLiveWorktreeEntries(listGitWorktrees(cwd));
277
+ }
251
278
  listLocalBranches(cwd) {
252
279
  return listLocalGitBranches(cwd);
253
280
  }
@@ -435,6 +462,7 @@ _webmux() {
435
462
  'service:Manage webmux as a system service'
436
463
  'update:Update webmux to the latest version'
437
464
  'add:Create a worktree'
465
+ 'oneshot:Run a worktree start-to-finish, streaming logs to stdout'
438
466
  'list:List worktrees and their status'
439
467
  'open:Open an existing worktree session'
440
468
  'close:Close a worktree session'
@@ -445,6 +473,7 @@ _webmux() {
445
473
  'merge:Merge a worktree into main'
446
474
  'send:Send a prompt to a running worktree agent'
447
475
  'prune:Remove all worktrees in the current project'
476
+ 'linear:Post a worktree conversation to a Linear issue/team'
448
477
  'completion:Generate shell completion script'
449
478
  )
450
479
 
@@ -463,6 +492,19 @@ _webmux() {
463
492
  fi
464
493
  fi
465
494
  ;;
495
+ linear)
496
+ if (( CURRENT == 3 )); then
497
+ local -a subs
498
+ subs=('post:Post a worktree conversation to a Linear issue or team')
499
+ _describe 'linear subcommand' subs
500
+ elif (( CURRENT == 4 )) && [[ "\${words[3]}" == "post" ]]; then
501
+ local -a branches
502
+ branches=(\${(f)"$(webmux --completions send 2>/dev/null)"})
503
+ if (( \${#branches} )); then
504
+ _describe 'worktree' branches
505
+ fi
506
+ fi
507
+ ;;
466
508
  completion)
467
509
  if (( CURRENT == 3 )); then
468
510
  local -a shells
@@ -492,7 +534,7 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
492
534
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
493
535
 
494
536
  if [[ \${COMP_CWORD} -eq 1 ]]; then
495
- COMPREPLY=($(compgen -W "serve init service update add list open close archive unarchive label remove merge send prune completion" -- "\${cur}"))
537
+ COMPREPLY=($(compgen -W "serve init service update add oneshot list open close archive unarchive label remove merge send prune linear completion" -- "\${cur}"))
496
538
  return
497
539
  fi
498
540
 
@@ -504,6 +546,15 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
504
546
  COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
505
547
  fi
506
548
  ;;
549
+ linear)
550
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
551
+ COMPREPLY=($(compgen -W "post" -- "\${cur}"))
552
+ elif [[ \${COMP_CWORD} -eq 3 ]] && [[ "\${COMP_WORDS[2]}" == "post" ]]; then
553
+ local branches
554
+ branches=$(webmux --completions send 2>/dev/null)
555
+ COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
556
+ fi
557
+ ;;
507
558
  completion)
508
559
  if [[ \${COMP_CWORD} -eq 2 ]]; then
509
560
  COMPREPLY=($(compgen -W "bash zsh" -- "\${cur}"))
@@ -1810,7 +1861,29 @@ function detectProjectName(gitRoot) {
1810
1861
  }
1811
1862
  return basename2(gitRoot);
1812
1863
  }
1813
- var init_shared = () => {};
1864
+ function formatServerError(error, port) {
1865
+ if (error instanceof Error) {
1866
+ if (error.message.startsWith("HTTP"))
1867
+ return error.message;
1868
+ if (error.message.includes("fetch")) {
1869
+ return `Could not connect to webmux server on port ${port}. Is it running?`;
1870
+ }
1871
+ return error.message;
1872
+ }
1873
+ return String(error);
1874
+ }
1875
+ async function withServerConnection(port, fn) {
1876
+ try {
1877
+ return await fn();
1878
+ } catch (error) {
1879
+ throw new Error(formatServerError(error, port));
1880
+ }
1881
+ }
1882
+ var CommandUsageError;
1883
+ var init_shared = __esm(() => {
1884
+ CommandUsageError = class CommandUsageError extends Error {
1885
+ };
1886
+ });
1814
1887
 
1815
1888
  // bin/src/init-helpers.ts
1816
1889
  import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
@@ -6943,7 +7016,7 @@ var init_zod = __esm(() => {
6943
7016
  init_external();
6944
7017
  });
6945
7018
 
6946
- // node_modules/.bun/@ts-rest+core@3.52.1+1c8a9bbc689bc595/node_modules/@ts-rest/core/index.esm.mjs
7019
+ // node_modules/.bun/@ts-rest+core@3.52.1+596964f7fee2c930/node_modules/@ts-rest/core/index.esm.mjs
6947
7020
  var isZodType = (obj) => {
6948
7021
  return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
6949
7022
  }, isZodObjectStrict = (obj) => {
@@ -7257,7 +7330,15 @@ var init_index_esm = __esm(() => {
7257
7330
  });
7258
7331
 
7259
7332
  // packages/api-contract/src/schemas.ts
7260
- var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, CreateWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, SetWorktreeLabelRequestSchema, SetWorktreeLabelResponseSchema, ToggleEnabledRequestSchema, SendWorktreePromptRequestSchema, AgentsSendMessageRequestSchema, PullMainRequestSchema, PullMainStatusSchema, PullMainResponseSchema, ServiceStatusSchema, PrCommentSchema, CiCheckSchema, PrEntrySchema, LinearIssueLabelSchema, LinearIssueStateSchema, LinkedLinearIssueSchema, LinearIssueSchema, LinearIssueAvailabilitySchema, LinearIssuesResponseSchema, WorktreeCreationStateSchema, AppNotificationSchema, ProjectWorktreeSnapshotSchema, ProjectSnapshotSchema, WorktreeConversationProviderSchema, CodexWorktreeConversationRefSchema, ClaudeWorktreeConversationRefSchema, WorktreeConversationRefSchema, AgentsUiWorktreeSummarySchema, AgentsUiConversationMessageRoleSchema, AgentsUiConversationMessageStatusSchema, AgentsUiConversationMessageSchema, AgentsUiConversationStateSchema, AgentsUiWorktreeConversationResponseSchema, AgentsUiSendMessageResponseSchema, AgentsUiInterruptResponseSchema, AgentsUiConversationSnapshotEventSchema, AgentsUiConversationMessageDeltaEventSchema, AgentsUiConversationErrorEventSchema, AgentsUiConversationEventSchema, WorktreeListResponseSchema, UnpushedCommitSchema, WorktreeDiffResponseSchema, ServiceConfigSchema, ProfileConfigSchema, LinkedRepoInfoSchema, AppConfigSchema, CiLogsResponseSchema, WorktreeNameParamsSchema, NotificationIdParamsSchema, AgentIdParamsSchema, RunIdParamsSchema;
7333
+ function parseLinearTarget(raw) {
7334
+ const trimmed = raw.trim();
7335
+ if (LinearIssueIdSchema.safeParse(trimmed).success)
7336
+ return { kind: "issue", issueId: trimmed };
7337
+ if (LinearTeamKeySchema.safeParse(trimmed).success)
7338
+ return { kind: "team", teamKey: trimmed };
7339
+ return { kind: "invalid", raw: trimmed };
7340
+ }
7341
+ var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, LinearIssueIdSchema, LinearTeamKeySchema, PostWorktreeToLinearTargetSchema, PostWorktreeToLinearRequestSchema, PostWorktreeToLinearResponseSchema, FromLinearInputSchema, OneshotConfigSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, WorktreeSourceSchema, CreateWorktreeRequestSchema, OpenWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, SetWorktreeLabelRequestSchema, SetWorktreeLabelResponseSchema, ToggleEnabledRequestSchema, SendWorktreePromptRequestSchema, AgentsSendMessageRequestSchema, PullMainRequestSchema, PullMainStatusSchema, PullMainResponseSchema, ServiceStatusSchema, PrCommentSchema, CiCheckSchema, PrEntrySchema, LinearIssueLabelSchema, LinearIssueStateSchema, LinkedLinearIssueSchema, LinearIssueSchema, LinearIssueAvailabilitySchema, LinearIssuesResponseSchema, WorktreeCreationStateSchema, AppNotificationSchema, ProjectWorktreeSnapshotSchema, ProjectSnapshotSchema, WorktreeConversationProviderSchema, CodexWorktreeConversationRefSchema, ClaudeWorktreeConversationRefSchema, WorktreeConversationRefSchema, AgentsUiWorktreeSummarySchema, AgentsUiConversationMessageRoleSchema, AgentsUiConversationMessageStatusSchema, AgentsUiConversationMessageKindSchema, AgentsUiConversationMessageSchema, AgentsUiConversationStateSchema, AgentsUiWorktreeConversationResponseSchema, AgentsUiSendMessageResponseSchema, AgentsUiInterruptResponseSchema, AgentsUiConversationSnapshotEventSchema, AgentsUiConversationMessageDeltaEventSchema, AgentsUiConversationErrorEventSchema, AgentsUiConversationEventSchema, WorktreeListResponseSchema, UnpushedCommitSchema, WorktreeDiffResponseSchema, ServiceConfigSchema, ProfileConfigSchema, LinkedRepoInfoSchema, AppConfigSchema, CiLogsResponseSchema, WorktreeNameParamsSchema, NotificationIdParamsSchema, AgentIdParamsSchema, RunIdParamsSchema;
7261
7342
  var init_schemas = __esm(() => {
7262
7343
  init_zod();
7263
7344
  BooleanLikeSchema = exports_external.union([
@@ -7278,6 +7359,30 @@ var init_schemas = __esm(() => {
7278
7359
  BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
7279
7360
  AgentIdSchema = exports_external.string().trim().min(1);
7280
7361
  WorktreeCreateModeSchema = exports_external.enum(["new", "existing"]);
7362
+ LinearIssueIdSchema = exports_external.string().regex(/^[A-Z]+-\d+$/, "Expected Linear issue id (e.g. ENG-123)");
7363
+ LinearTeamKeySchema = exports_external.string().regex(/^[A-Z]+$/, "Expected Linear team key (e.g. ENG)");
7364
+ PostWorktreeToLinearTargetSchema = exports_external.discriminatedUnion("kind", [
7365
+ exports_external.object({ kind: exports_external.literal("issue"), issueId: LinearIssueIdSchema }),
7366
+ exports_external.object({ kind: exports_external.literal("team"), teamKey: LinearTeamKeySchema, title: exports_external.string().trim().min(1).optional() })
7367
+ ]);
7368
+ PostWorktreeToLinearRequestSchema = exports_external.object({
7369
+ target: PostWorktreeToLinearTargetSchema
7370
+ });
7371
+ PostWorktreeToLinearResponseSchema = exports_external.object({
7372
+ ok: exports_external.literal(true),
7373
+ issueId: exports_external.string(),
7374
+ issueUrl: exports_external.string(),
7375
+ commentUrl: exports_external.string().nullable(),
7376
+ attachmentUrl: exports_external.string()
7377
+ });
7378
+ FromLinearInputSchema = exports_external.object({
7379
+ issueId: LinearIssueIdSchema,
7380
+ conversationContext: exports_external.string().optional()
7381
+ });
7382
+ OneshotConfigSchema = exports_external.object({
7383
+ autoCloseOnDone: exports_external.boolean().optional(),
7384
+ postToLinearOnDone: PostWorktreeToLinearTargetSchema.optional()
7385
+ });
7281
7386
  AgentCapabilitiesSchema = exports_external.object({
7282
7387
  terminal: exports_external.literal(true),
7283
7388
  inAppChat: exports_external.boolean(),
@@ -7334,6 +7439,7 @@ var init_schemas = __esm(() => {
7334
7439
  BranchListResponseSchema = exports_external.object({
7335
7440
  branches: exports_external.array(AvailableBranchSchema)
7336
7441
  });
7442
+ WorktreeSourceSchema = exports_external.enum(["ui", "oneshot"]);
7337
7443
  CreateWorktreeRequestSchema = exports_external.object({
7338
7444
  mode: WorktreeCreateModeSchema.optional(),
7339
7445
  branch: exports_external.string().optional(),
@@ -7344,7 +7450,14 @@ var init_schemas = __esm(() => {
7344
7450
  prompt: exports_external.string().optional(),
7345
7451
  envOverrides: exports_external.record(exports_external.string()).optional(),
7346
7452
  createLinearTicket: exports_external.literal(true).optional(),
7347
- linearTitle: exports_external.string().optional()
7453
+ linearTitle: exports_external.string().optional(),
7454
+ fromLinear: FromLinearInputSchema.optional(),
7455
+ source: WorktreeSourceSchema.optional(),
7456
+ oneshot: OneshotConfigSchema.optional()
7457
+ });
7458
+ OpenWorktreeRequestSchema = exports_external.object({
7459
+ prompt: exports_external.string().optional(),
7460
+ oneshot: OneshotConfigSchema.optional()
7348
7461
  });
7349
7462
  CreateWorktreeResponseSchema = exports_external.object({
7350
7463
  primaryBranch: exports_external.string(),
@@ -7490,7 +7603,9 @@ var init_schemas = __esm(() => {
7490
7603
  services: exports_external.array(ServiceStatusSchema),
7491
7604
  prs: exports_external.array(PrEntrySchema),
7492
7605
  linearIssue: LinkedLinearIssueSchema.nullable(),
7493
- creation: WorktreeCreationStateSchema.nullable()
7606
+ creation: WorktreeCreationStateSchema.nullable(),
7607
+ source: WorktreeSourceSchema,
7608
+ oneshot: OneshotConfigSchema.nullable()
7494
7609
  });
7495
7610
  ProjectSnapshotSchema = exports_external.object({
7496
7611
  project: exports_external.object({
@@ -7539,13 +7654,16 @@ var init_schemas = __esm(() => {
7539
7654
  });
7540
7655
  AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
7541
7656
  AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
7657
+ AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
7542
7658
  AgentsUiConversationMessageSchema = exports_external.object({
7543
7659
  id: exports_external.string(),
7544
7660
  turnId: exports_external.string(),
7545
7661
  role: AgentsUiConversationMessageRoleSchema,
7546
7662
  text: exports_external.string(),
7547
7663
  status: AgentsUiConversationMessageStatusSchema,
7548
- createdAt: exports_external.string().nullable()
7664
+ createdAt: exports_external.string().nullable(),
7665
+ kind: AgentsUiConversationMessageKindSchema.optional(),
7666
+ toolName: exports_external.string().optional()
7549
7667
  });
7550
7668
  AgentsUiConversationStateSchema = exports_external.object({
7551
7669
  provider: WorktreeConversationProviderSchema,
@@ -7674,6 +7792,8 @@ var init_contract = __esm(() => {
7674
7792
  openWorktree: "/api/worktrees/:name/open",
7675
7793
  closeWorktree: "/api/worktrees/:name/close",
7676
7794
  setWorktreeArchived: "/api/worktrees/:name/archive",
7795
+ syncWorktreePrs: "/api/worktrees/:name/sync-prs",
7796
+ postWorktreeToLinear: "/api/worktrees/:name/linear/post",
7677
7797
  setWorktreeLabel: "/api/worktrees/:name/label",
7678
7798
  sendWorktreePrompt: "/api/worktrees/:name/send",
7679
7799
  mergeWorktree: "/api/worktrees/:name/merge",
@@ -7852,7 +7972,7 @@ var init_contract = __esm(() => {
7852
7972
  method: "POST",
7853
7973
  path: apiPaths.openWorktree,
7854
7974
  pathParams: WorktreeNameParamsSchema,
7855
- body: c.noBody(),
7975
+ body: OpenWorktreeRequestSchema,
7856
7976
  responses: {
7857
7977
  200: OkResponseSchema,
7858
7978
  ...commonErrorResponses
@@ -7878,6 +7998,26 @@ var init_contract = __esm(() => {
7878
7998
  ...commonErrorResponses
7879
7999
  }
7880
8000
  },
8001
+ postWorktreeToLinear: {
8002
+ method: "POST",
8003
+ path: apiPaths.postWorktreeToLinear,
8004
+ pathParams: WorktreeNameParamsSchema,
8005
+ body: PostWorktreeToLinearRequestSchema,
8006
+ responses: {
8007
+ 200: PostWorktreeToLinearResponseSchema,
8008
+ ...commonErrorResponses
8009
+ }
8010
+ },
8011
+ syncWorktreePrs: {
8012
+ method: "POST",
8013
+ path: apiPaths.syncWorktreePrs,
8014
+ pathParams: WorktreeNameParamsSchema,
8015
+ body: c.noBody(),
8016
+ responses: {
8017
+ 200: ProjectWorktreeSnapshotSchema,
8018
+ ...commonErrorResponses
8019
+ }
8020
+ },
7881
8021
  setWorktreeLabel: {
7882
8022
  method: "PUT",
7883
8023
  path: apiPaths.setWorktreeLabel,
@@ -7935,128 +8075,1464 @@ var init_contract = __esm(() => {
7935
8075
  ...commonErrorResponses
7936
8076
  }
7937
8077
  },
7938
- setAutoRemoveOnMerge: {
7939
- method: "PUT",
7940
- path: apiPaths.setAutoRemoveOnMerge,
7941
- body: ToggleEnabledRequestSchema,
7942
- responses: {
7943
- 200: EnabledResponseSchema,
7944
- ...commonErrorResponses
7945
- }
8078
+ setAutoRemoveOnMerge: {
8079
+ method: "PUT",
8080
+ path: apiPaths.setAutoRemoveOnMerge,
8081
+ body: ToggleEnabledRequestSchema,
8082
+ responses: {
8083
+ 200: EnabledResponseSchema,
8084
+ ...commonErrorResponses
8085
+ }
8086
+ },
8087
+ pullMain: {
8088
+ method: "POST",
8089
+ path: apiPaths.pullMain,
8090
+ body: PullMainRequestSchema,
8091
+ responses: {
8092
+ 200: PullMainResponseSchema,
8093
+ ...commonErrorResponses
8094
+ }
8095
+ },
8096
+ fetchCiLogs: {
8097
+ method: "GET",
8098
+ path: apiPaths.fetchCiLogs,
8099
+ pathParams: RunIdParamsSchema,
8100
+ responses: {
8101
+ 200: CiLogsResponseSchema,
8102
+ ...commonErrorResponses
8103
+ }
8104
+ },
8105
+ dismissNotification: {
8106
+ method: "POST",
8107
+ path: apiPaths.dismissNotification,
8108
+ pathParams: NotificationIdParamsSchema,
8109
+ body: c.noBody(),
8110
+ responses: {
8111
+ 200: OkResponseSchema,
8112
+ 400: ErrorResponseSchema,
8113
+ 404: ErrorResponseSchema
8114
+ }
8115
+ }
8116
+ }, {
8117
+ strictStatusCodes: true
8118
+ });
8119
+ });
8120
+
8121
+ // packages/api-contract/src/client.ts
8122
+ function createApiClient(baseUrl, options = {}) {
8123
+ return initClient(apiContract, {
8124
+ baseUrl,
8125
+ throwOnUnknownStatus: true,
8126
+ baseHeaders: {},
8127
+ ...options
8128
+ });
8129
+ }
8130
+ function isRecord2(value) {
8131
+ return typeof value === "object" && value !== null;
8132
+ }
8133
+ function isRouteResponse(value) {
8134
+ return isRecord2(value) && "status" in value && typeof value.status === "number" && "body" in value;
8135
+ }
8136
+ function unwrapResponse(response) {
8137
+ if (!isRouteResponse(response)) {
8138
+ throw new Error("Malformed API client response");
8139
+ }
8140
+ if (response.status < 200 || response.status >= 300) {
8141
+ throw new Error(errorMessageFromResponse(response.body, response.status));
8142
+ }
8143
+ return response.body;
8144
+ }
8145
+ function errorMessageFromResponse(body, status2) {
8146
+ if (typeof body === "string") {
8147
+ try {
8148
+ const parsed = JSON.parse(body);
8149
+ return errorMessageFromResponse(parsed, status2);
8150
+ } catch {
8151
+ return body.trim() || `HTTP ${status2}`;
8152
+ }
8153
+ }
8154
+ if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
8155
+ return body.error;
8156
+ }
8157
+ return `HTTP ${status2}`;
8158
+ }
8159
+ function withEncodedPathParams(args) {
8160
+ const [first, ...rest] = args;
8161
+ if (!first || typeof first !== "object" || !("params" in first) || !first.params || typeof first.params !== "object") {
8162
+ return args;
8163
+ }
8164
+ const encodedParams = Object.fromEntries(Object.entries(first.params).map(([key, value]) => [key, encodeURIComponent(String(value))]));
8165
+ return [
8166
+ {
8167
+ ...first,
8168
+ params: encodedParams
8169
+ },
8170
+ ...rest
8171
+ ];
8172
+ }
8173
+ function wrapRouteCall(routeCall) {
8174
+ return async (...args) => unwrapResponse(await routeCall(...withEncodedPathParams(args)));
8175
+ }
8176
+ function wrapClient(client) {
8177
+ return Object.fromEntries(Object.entries(client).map(([key, value]) => {
8178
+ if (typeof value === "function") {
8179
+ return [key, wrapRouteCall((...args) => Promise.resolve(Reflect.apply(value, undefined, args)))];
8180
+ }
8181
+ if (isRecord2(value)) {
8182
+ return [key, wrapClient(value)];
8183
+ }
8184
+ return [key, value];
8185
+ }));
8186
+ }
8187
+ function createApi(baseUrl, options = {}) {
8188
+ return wrapClient(createApiClient(baseUrl, options));
8189
+ }
8190
+ var init_client = __esm(() => {
8191
+ init_index_esm();
8192
+ init_contract();
8193
+ });
8194
+
8195
+ // packages/api-contract/src/index.ts
8196
+ var init_src = __esm(() => {
8197
+ init_contract();
8198
+ init_client();
8199
+ init_schemas();
8200
+ });
8201
+
8202
+ // backend/src/lib/log.ts
8203
+ function ts() {
8204
+ return new Date().toISOString().slice(11, 23);
8205
+ }
8206
+ var DEBUG, log;
8207
+ var init_log = __esm(() => {
8208
+ DEBUG = Bun.env.WEBMUX_DEBUG === "1";
8209
+ log = {
8210
+ info(msg) {
8211
+ console.log(`[${ts()}] ${msg}`);
8212
+ },
8213
+ debug(msg) {
8214
+ if (DEBUG)
8215
+ console.log(`[${ts()}] ${msg}`);
8216
+ },
8217
+ warn(msg) {
8218
+ console.warn(`[${ts()}] ${msg}`);
8219
+ },
8220
+ error(msg, err) {
8221
+ err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
8222
+ }
8223
+ };
8224
+ });
8225
+
8226
+ // backend/src/services/linear-service.ts
8227
+ function gqlErrorMessage(raw) {
8228
+ return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
8229
+ }
8230
+ function parseViewerIdResponse(raw) {
8231
+ const error = gqlErrorMessage(raw);
8232
+ if (error) {
8233
+ return { ok: false, error };
8234
+ }
8235
+ const viewerId = raw.data?.viewer.id;
8236
+ if (!viewerId) {
8237
+ return { ok: false, error: "No viewer id in response" };
8238
+ }
8239
+ return { ok: true, data: viewerId };
8240
+ }
8241
+ function parseInProgressStateIdResponse(raw) {
8242
+ const error = gqlErrorMessage(raw);
8243
+ if (error) {
8244
+ return { ok: false, error };
8245
+ }
8246
+ const states = raw.data?.team?.states.nodes;
8247
+ if (!states) {
8248
+ return { ok: false, error: "No team states in response" };
8249
+ }
8250
+ const preferredState = states.find((state) => state.type === "started" && state.name.trim().toLowerCase() === "in progress");
8251
+ if (preferredState) {
8252
+ return { ok: true, data: preferredState.id };
8253
+ }
8254
+ const startedState = states.find((state) => state.type === "started");
8255
+ if (!startedState) {
8256
+ return { ok: false, error: "No started workflow state found for team" };
8257
+ }
8258
+ return { ok: true, data: startedState.id };
8259
+ }
8260
+ function parseIssueCreateResponse(raw) {
8261
+ const error = gqlErrorMessage(raw);
8262
+ if (error) {
8263
+ return { ok: false, error };
8264
+ }
8265
+ const payload = raw.data?.issueCreate;
8266
+ if (!payload) {
8267
+ return { ok: false, error: "No issueCreate payload in response" };
8268
+ }
8269
+ if (!payload.success || !payload.issue) {
8270
+ return { ok: false, error: "Linear issue creation was not successful" };
8271
+ }
8272
+ if (!payload.issue.branchName) {
8273
+ return { ok: false, error: "Linear issue did not return a branch name" };
8274
+ }
8275
+ return {
8276
+ ok: true,
8277
+ data: {
8278
+ id: payload.issue.id,
8279
+ identifier: payload.issue.identifier,
8280
+ title: payload.issue.title,
8281
+ url: payload.issue.url,
8282
+ branchName: payload.issue.branchName
8283
+ }
8284
+ };
8285
+ }
8286
+ async function postLinearGraphql(query, variables) {
8287
+ const apiKey = Bun.env.LINEAR_API_KEY;
8288
+ if (!apiKey) {
8289
+ return { ok: false, error: "LINEAR_API_KEY not set" };
8290
+ }
8291
+ try {
8292
+ const res = await fetch("https://api.linear.app/graphql", {
8293
+ method: "POST",
8294
+ headers: {
8295
+ "Content-Type": "application/json",
8296
+ Authorization: apiKey
8297
+ },
8298
+ body: JSON.stringify(variables ? { query, variables } : { query })
8299
+ });
8300
+ if (!res.ok) {
8301
+ const text = await res.text();
8302
+ return { ok: false, error: `Linear API ${res.status}: ${text.slice(0, 200)}` };
8303
+ }
8304
+ return {
8305
+ ok: true,
8306
+ data: await res.json()
8307
+ };
8308
+ } catch (err) {
8309
+ const msg = err instanceof Error ? err.message : String(err);
8310
+ return { ok: false, error: msg };
8311
+ }
8312
+ }
8313
+ async function fetchViewerId() {
8314
+ if (viewerIdCache) {
8315
+ return { ok: true, data: viewerIdCache };
8316
+ }
8317
+ const response = await postLinearGraphql(VIEWER_QUERY);
8318
+ if (!response.ok) {
8319
+ log.error(`[linear] viewer fetch failed: ${response.error}`);
8320
+ return { ok: false, error: response.error };
8321
+ }
8322
+ const result = parseViewerIdResponse(response.data);
8323
+ if (!result.ok) {
8324
+ log.error(`[linear] viewer GraphQL error: ${result.error}`);
8325
+ return result;
8326
+ }
8327
+ viewerIdCache = result.data;
8328
+ return result;
8329
+ }
8330
+ async function fetchInProgressStateId(teamId) {
8331
+ const cachedStateId = inProgressStateIdCache.get(teamId);
8332
+ if (cachedStateId) {
8333
+ return { ok: true, data: cachedStateId };
8334
+ }
8335
+ const response = await postLinearGraphql(TEAM_STATES_QUERY, { teamId });
8336
+ if (!response.ok) {
8337
+ log.error(`[linear] team states fetch failed: ${response.error}`);
8338
+ return { ok: false, error: response.error };
8339
+ }
8340
+ const result = parseInProgressStateIdResponse(response.data);
8341
+ if (!result.ok) {
8342
+ log.error(`[linear] team states GraphQL error: ${result.error}`);
8343
+ return result;
8344
+ }
8345
+ inProgressStateIdCache.set(teamId, result.data);
8346
+ return result;
8347
+ }
8348
+ function buildWebmuxAttachmentTitle(branch) {
8349
+ return `${WEBMUX_ATTACHMENT_TITLE_PREFIX}${branch}`;
8350
+ }
8351
+ function findWebmuxAttachment(issue, branch) {
8352
+ const candidates = issue.attachments.filter((a) => a.title.startsWith(WEBMUX_ATTACHMENT_TITLE_PREFIX));
8353
+ if (candidates.length === 0)
8354
+ return null;
8355
+ if (branch) {
8356
+ const exact = candidates.find((a) => a.title === buildWebmuxAttachmentTitle(branch));
8357
+ if (exact)
8358
+ return exact;
8359
+ }
8360
+ return [...candidates].sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] ?? null;
8361
+ }
8362
+ function inferPrStateFromAttachment(attachment) {
8363
+ const meta = attachment.metadata ?? {};
8364
+ const rawState = typeof meta.state === "string" ? meta.state.toLowerCase() : null;
8365
+ if (rawState === "open" || rawState === "closed" || rawState === "merged")
8366
+ return rawState;
8367
+ const status2 = typeof meta.status === "string" ? meta.status.toLowerCase() : null;
8368
+ if (status2 === "open" || status2 === "closed" || status2 === "merged")
8369
+ return status2;
8370
+ return "unknown";
8371
+ }
8372
+ function inferPrBranchFromAttachment(attachment) {
8373
+ const meta = attachment.metadata ?? {};
8374
+ if (typeof meta.branchName === "string" && meta.branchName.trim())
8375
+ return meta.branchName.trim();
8376
+ if (typeof meta.headRefName === "string" && meta.headRefName.trim())
8377
+ return meta.headRefName.trim();
8378
+ return null;
8379
+ }
8380
+ function findLinkedGitHubPr(issue) {
8381
+ const githubAttachments = issue.attachments.filter((a) => {
8382
+ if (a.sourceType === "github" || a.sourceType === "githubPR" || a.sourceType === "github_pull_request") {
8383
+ return true;
8384
+ }
8385
+ return /github\.com\/.+\/pull\/\d+/i.test(a.url);
8386
+ });
8387
+ if (githubAttachments.length === 0)
8388
+ return null;
8389
+ const prs = githubAttachments.map((a) => ({
8390
+ url: a.url,
8391
+ branch: inferPrBranchFromAttachment(a),
8392
+ state: inferPrStateFromAttachment(a)
8393
+ }));
8394
+ const indexed = prs.map((pr, idx) => ({ pr, idx, attachment: githubAttachments[idx] }));
8395
+ indexed.sort((a, b) => {
8396
+ const stateDiff = STATE_PRIORITY[a.pr.state] - STATE_PRIORITY[b.pr.state];
8397
+ if (stateDiff !== 0)
8398
+ return stateDiff;
8399
+ return b.attachment.createdAt.localeCompare(a.attachment.createdAt);
8400
+ });
8401
+ return indexed[0].pr;
8402
+ }
8403
+ async function fetchIssueWithAttachments(issueIdentifierOrId) {
8404
+ const response = await postLinearGraphql(ISSUE_WITH_ATTACHMENTS_QUERY, {
8405
+ id: issueIdentifierOrId
8406
+ });
8407
+ if (!response.ok) {
8408
+ return { ok: false, error: response.error, status: 502 };
8409
+ }
8410
+ const error = gqlErrorMessage(response.data);
8411
+ if (error) {
8412
+ return { ok: false, error, status: 502 };
8413
+ }
8414
+ const issue = response.data.data?.issue;
8415
+ if (!issue) {
8416
+ return { ok: false, error: `Linear issue not found: ${issueIdentifierOrId}`, status: 404 };
8417
+ }
8418
+ return {
8419
+ ok: true,
8420
+ data: {
8421
+ id: issue.id,
8422
+ identifier: issue.identifier,
8423
+ title: issue.title,
8424
+ description: issue.description,
8425
+ url: issue.url,
8426
+ branchName: issue.branchName,
8427
+ attachments: issue.attachments.nodes.map((node) => ({
8428
+ id: node.id,
8429
+ url: node.url,
8430
+ title: node.title,
8431
+ subtitle: node.subtitle,
8432
+ sourceType: node.sourceType,
8433
+ metadata: node.metadata,
8434
+ createdAt: node.createdAt
8435
+ }))
8436
+ }
8437
+ };
8438
+ }
8439
+ async function fetchTeamByKey(teamKey) {
8440
+ const response = await postLinearGraphql(TEAM_BY_KEY_QUERY, { key: teamKey });
8441
+ if (!response.ok) {
8442
+ return { ok: false, error: response.error, status: 502 };
8443
+ }
8444
+ const error = gqlErrorMessage(response.data);
8445
+ if (error) {
8446
+ return { ok: false, error, status: 502 };
8447
+ }
8448
+ const team = response.data.data?.teams.nodes[0];
8449
+ if (!team) {
8450
+ return { ok: false, error: `Linear team not found for key: ${teamKey}`, status: 404 };
8451
+ }
8452
+ return { ok: true, data: team };
8453
+ }
8454
+ async function createLinearIssue(input) {
8455
+ const viewerResult = await fetchViewerId();
8456
+ if (!viewerResult.ok) {
8457
+ return { ok: false, error: viewerResult.error };
8458
+ }
8459
+ const stateResult = await fetchInProgressStateId(input.teamId);
8460
+ if (!stateResult.ok) {
8461
+ return { ok: false, error: stateResult.error };
8462
+ }
8463
+ const response = await postLinearGraphql(ISSUE_CREATE_MUTATION, {
8464
+ input: {
8465
+ title: input.title,
8466
+ description: input.description,
8467
+ teamId: input.teamId,
8468
+ assigneeId: viewerResult.data,
8469
+ stateId: stateResult.data
8470
+ }
8471
+ });
8472
+ if (!response.ok) {
8473
+ log.error(`[linear] create failed: ${response.error}`);
8474
+ return { ok: false, error: response.error };
8475
+ }
8476
+ const result = parseIssueCreateResponse(response.data);
8477
+ if (result.ok) {
8478
+ issueCache = null;
8479
+ log.debug(`[linear] created issue ${result.data.identifier} branch=${result.data.branchName}`);
8480
+ } else {
8481
+ log.error(`[linear] issueCreate error: ${result.error}`);
8482
+ }
8483
+ return result;
8484
+ }
8485
+ var VIEWER_QUERY = `
8486
+ query Viewer {
8487
+ viewer {
8488
+ id
8489
+ }
8490
+ }
8491
+ `, TEAM_STATES_QUERY = `
8492
+ query TeamStates($teamId: String!) {
8493
+ team(id: $teamId) {
8494
+ states {
8495
+ nodes {
8496
+ id
8497
+ name
8498
+ type
8499
+ }
8500
+ }
8501
+ }
8502
+ }
8503
+ `, ISSUE_CREATE_MUTATION = `
8504
+ mutation IssueCreate($input: IssueCreateInput!) {
8505
+ issueCreate(input: $input) {
8506
+ success
8507
+ issue {
8508
+ id
8509
+ identifier
8510
+ title
8511
+ url
8512
+ branchName
8513
+ }
8514
+ }
8515
+ }
8516
+ `, issueCache = null, viewerIdCache = null, inProgressStateIdCache, ISSUE_WITH_ATTACHMENTS_QUERY = `
8517
+ query IssueWithAttachments($id: String!) {
8518
+ issue(id: $id) {
8519
+ id
8520
+ identifier
8521
+ title
8522
+ description
8523
+ url
8524
+ branchName
8525
+ attachments {
8526
+ nodes {
8527
+ id
8528
+ url
8529
+ title
8530
+ subtitle
8531
+ sourceType
8532
+ metadata
8533
+ createdAt
8534
+ }
8535
+ }
8536
+ }
8537
+ }
8538
+ `, TEAM_BY_KEY_QUERY = `
8539
+ query TeamByKey($key: String!) {
8540
+ teams(filter: { key: { eq: $key } }, first: 1) {
8541
+ nodes {
8542
+ id
8543
+ key
8544
+ name
8545
+ }
8546
+ }
8547
+ }
8548
+ `, WEBMUX_ATTACHMENT_TITLE_PREFIX = "webmux-state:", STATE_PRIORITY;
8549
+ var init_linear_service = __esm(() => {
8550
+ init_log();
8551
+ init_src();
8552
+ inProgressStateIdCache = new Map;
8553
+ STATE_PRIORITY = {
8554
+ open: 0,
8555
+ merged: 1,
8556
+ closed: 2,
8557
+ unknown: 3
8558
+ };
8559
+ });
8560
+
8561
+ // backend/src/services/conversation-export-service.ts
8562
+ function escapeFence(text) {
8563
+ return text.replace(/```/g, "``\u200B`");
8564
+ }
8565
+ function buildIssueHeader(issue) {
8566
+ const lines = [];
8567
+ lines.push(`This worktree is for Linear issue **${issue.identifier}** \u2014 ${issue.url}`);
8568
+ lines.push("");
8569
+ 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}\`).`);
8570
+ lines.push("");
8571
+ lines.push(`## Issue: ${issue.title}`);
8572
+ if (issue.description?.trim()) {
8573
+ lines.push("");
8574
+ lines.push(escapeFence(issue.description.trim()));
8575
+ }
8576
+ lines.push("");
8577
+ return lines.join(`
8578
+ `);
8579
+ }
8580
+ function buildPriorConversationSection(payload) {
8581
+ const lines = [];
8582
+ lines.push(`---`);
8583
+ lines.push("");
8584
+ lines.push(`A previous webmux session for this issue was saved here (branch \`${payload.branch}\`${payload.baseBranch ? `, base \`${payload.baseBranch}\`` : ""}).`);
8585
+ lines.push("");
8586
+ lines.push("Previous conversation (chronological):");
8587
+ lines.push("");
8588
+ for (const message of payload.conversation) {
8589
+ lines.push(`### ${message.role}`);
8590
+ lines.push("");
8591
+ lines.push(escapeFence(message.text));
8592
+ lines.push("");
8593
+ }
8594
+ return lines.join(`
8595
+ `);
8596
+ }
8597
+ async function buildSeedFromLinear(input, deps2) {
8598
+ const issue = await deps2.fetchIssueWithAttachments(input.issueId);
8599
+ if (!issue.ok)
8600
+ return issue;
8601
+ const issueHeader = buildIssueHeader(issue.data);
8602
+ const webmuxAttachment = findWebmuxAttachment(issue.data, input.preferBranch);
8603
+ const pr = findLinkedGitHubPr(issue.data);
8604
+ let attachmentPayload = null;
8605
+ if (webmuxAttachment) {
8606
+ const payloadResult = await deps2.downloadWebmuxAttachment(webmuxAttachment.url);
8607
+ if (payloadResult.ok) {
8608
+ attachmentPayload = payloadResult.data;
8609
+ } else {
8610
+ log.error(`[linear] webmux attachment download failed: ${payloadResult.error}`);
8611
+ }
8612
+ }
8613
+ const source = attachmentPayload ? "webmux-attachment" : pr ? "github-integration" : "none";
8614
+ const branch = attachmentPayload?.branch ?? pr?.branch ?? (issue.data.branchName || null);
8615
+ const baseBranch = attachmentPayload?.baseBranch ?? null;
8616
+ const conversationMarkdown = attachmentPayload ? `${issueHeader}${buildPriorConversationSection(attachmentPayload)}` : issueHeader;
8617
+ return {
8618
+ ok: true,
8619
+ data: {
8620
+ source,
8621
+ branch,
8622
+ baseBranch,
8623
+ prUrl: pr?.url ?? null,
8624
+ conversationMarkdown
8625
+ }
8626
+ };
8627
+ }
8628
+ async function downloadWebmuxAttachmentDefault(url) {
8629
+ const apiKey = Bun.env.LINEAR_API_KEY;
8630
+ if (!apiKey)
8631
+ return { ok: false, error: "LINEAR_API_KEY not set" };
8632
+ try {
8633
+ const res = await fetch(url, {
8634
+ headers: { Authorization: apiKey }
8635
+ });
8636
+ if (!res.ok) {
8637
+ return { ok: false, error: `Asset download failed ${res.status}` };
8638
+ }
8639
+ const text = await res.text();
8640
+ const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
8641
+ if (!parsed.success) {
8642
+ return { ok: false, error: "Asset is not a webmux conversation payload" };
8643
+ }
8644
+ return { ok: true, data: parsed.data };
8645
+ } catch (err) {
8646
+ const msg = err instanceof Error ? err.message : String(err);
8647
+ return { ok: false, error: msg };
8648
+ }
8649
+ }
8650
+ var WebmuxConversationAttachmentPayloadSchema, defaultSeedFromLinearDeps;
8651
+ var init_conversation_export_service = __esm(() => {
8652
+ init_src();
8653
+ init_zod();
8654
+ init_log();
8655
+ init_linear_service();
8656
+ WebmuxConversationAttachmentPayloadSchema = exports_external.object({
8657
+ webmux: exports_external.literal(1),
8658
+ branch: exports_external.string(),
8659
+ baseBranch: exports_external.string().nullable(),
8660
+ agent: AgentIdSchema.nullable(),
8661
+ createdAt: exports_external.string(),
8662
+ conversation: exports_external.array(AgentsUiConversationMessageSchema)
8663
+ });
8664
+ defaultSeedFromLinearDeps = {
8665
+ fetchIssueWithAttachments,
8666
+ downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
8667
+ };
8668
+ });
8669
+
8670
+ // bin/src/oneshot.ts
8671
+ var exports_oneshot = {};
8672
+ __export(exports_oneshot, {
8673
+ runOneshotCommand: () => runOneshotCommand,
8674
+ runOneshot: () => runOneshot,
8675
+ parseOneshotArgs: () => parseOneshotArgs,
8676
+ getOneshotUsage: () => getOneshotUsage
8677
+ });
8678
+ function getOneshotUsage() {
8679
+ return [
8680
+ "Usage:",
8681
+ " webmux oneshot [branch] --prompt <text> [--agent <id>] [--base <branch>] [--profile <name>]",
8682
+ " [--env KEY=VALUE]... [--keep-open] [--linear <issue-id|team-key>]",
8683
+ " webmux oneshot --resume <branch> --prompt <text>",
8684
+ "",
8685
+ "Runs an agent worktree start-to-finish, streaming the conversation to stdout.",
8686
+ "Does not change the focused tmux session. The server-side oneshot watcher",
8687
+ "closes the worktree session (and posts the conversation back to Linear, if",
8688
+ "--linear is set) once the agent finishes \u2014 even if this CLI is killed mid-run.",
8689
+ "Opening the worktree in the browser and interacting with it disarms the watcher.",
8690
+ "",
8691
+ "Exit codes: 0 if the agent opened a PR / the user took over via the browser;",
8692
+ "1 if the agent went idle without opening a PR; 130 on Ctrl-C (worktree keeps",
8693
+ "running, resume with `webmux oneshot --resume <branch>`).",
8694
+ "",
8695
+ "Options:",
8696
+ " --resume <branch> Resume an existing local worktree instead of creating one",
8697
+ " --prompt <text> Initial agent prompt (required; follow-up nudge when --resume)",
8698
+ " --agent <id> Agent id to launch",
8699
+ " --base <branch> Base branch for a new worktree (defaults to config)",
8700
+ " --profile <name> Worktree profile from .webmux.yaml",
8701
+ " --env KEY=VALUE Runtime env override (repeatable)",
8702
+ " --keep-open Don't auto-close the worktree session when the agent finishes",
8703
+ " --linear ID|TEAM Tie this oneshot to Linear:",
8704
+ " ENG-123 \u2014 load the issue body as context, post results back",
8705
+ " ENG \u2014 create a new issue in that team when done",
8706
+ " --branch <name> Override the branch when --linear resolves to one",
8707
+ " --help Show this help message"
8708
+ ].join(`
8709
+ `);
8710
+ }
8711
+ function readOptionValue(args, index, flag) {
8712
+ const arg = args[index];
8713
+ if (!arg)
8714
+ throw new CommandUsageError(`${flag} requires a value`);
8715
+ const prefix = `${flag}=`;
8716
+ if (arg.startsWith(prefix))
8717
+ return { value: arg.slice(prefix.length), nextIndex: index };
8718
+ const value = args[index + 1];
8719
+ if (value === undefined)
8720
+ throw new CommandUsageError(`${flag} requires a value`);
8721
+ return { value, nextIndex: index + 1 };
8722
+ }
8723
+ function parseOneshotArgs(args) {
8724
+ const body = {};
8725
+ const envOverrides = {};
8726
+ let branch = null;
8727
+ let branchFlagUsed = false;
8728
+ let prompt = null;
8729
+ let resume = false;
8730
+ let resumeBranch = null;
8731
+ let keepOpen = false;
8732
+ let fromLinearIssueId = null;
8733
+ let postToLinearTarget = null;
8734
+ for (let index = 0;index < args.length; index++) {
8735
+ const arg = args[index];
8736
+ if (!arg)
8737
+ continue;
8738
+ if (arg === "--help" || arg === "-h")
8739
+ return null;
8740
+ if (arg === "--resume" || arg.startsWith("--resume=")) {
8741
+ const { value, nextIndex } = readOptionValue(args, index, "--resume");
8742
+ resume = true;
8743
+ resumeBranch = value.trim();
8744
+ index = nextIndex;
8745
+ continue;
8746
+ }
8747
+ if (arg === "--prompt" || arg.startsWith("--prompt=")) {
8748
+ const { value, nextIndex } = readOptionValue(args, index, "--prompt");
8749
+ prompt = value;
8750
+ index = nextIndex;
8751
+ continue;
8752
+ }
8753
+ if (arg === "--agent" || arg.startsWith("--agent=")) {
8754
+ const { value, nextIndex } = readOptionValue(args, index, "--agent");
8755
+ body.agent = value.trim();
8756
+ index = nextIndex;
8757
+ continue;
8758
+ }
8759
+ if (arg === "--base" || arg.startsWith("--base=")) {
8760
+ const { value, nextIndex } = readOptionValue(args, index, "--base");
8761
+ body.baseBranch = value;
8762
+ index = nextIndex;
8763
+ continue;
8764
+ }
8765
+ if (arg === "--profile" || arg.startsWith("--profile=")) {
8766
+ const { value, nextIndex } = readOptionValue(args, index, "--profile");
8767
+ body.profile = value;
8768
+ index = nextIndex;
8769
+ continue;
8770
+ }
8771
+ if (arg === "--env" || arg.startsWith("--env=")) {
8772
+ const { value, nextIndex } = readOptionValue(args, index, "--env");
8773
+ const sep = value.indexOf("=");
8774
+ if (sep <= 0)
8775
+ throw new CommandUsageError("--env must use KEY=VALUE");
8776
+ envOverrides[value.slice(0, sep)] = value.slice(sep + 1);
8777
+ index = nextIndex;
8778
+ continue;
8779
+ }
8780
+ if (arg === "--keep-open") {
8781
+ keepOpen = true;
8782
+ continue;
8783
+ }
8784
+ if (arg === "--linear" || arg.startsWith("--linear=")) {
8785
+ const { value, nextIndex } = readOptionValue(args, index, "--linear");
8786
+ const target = parseLinearTarget(value);
8787
+ if (target.kind === "issue") {
8788
+ fromLinearIssueId = target.issueId;
8789
+ postToLinearTarget = { kind: "issue", issueId: target.issueId };
8790
+ } else if (target.kind === "team") {
8791
+ postToLinearTarget = { kind: "team", teamKey: target.teamKey };
8792
+ } else {
8793
+ throw new CommandUsageError(`--linear expects either an issue id (ENG-123) or a team key (ENG); got "${target.raw}"`);
8794
+ }
8795
+ index = nextIndex;
8796
+ continue;
8797
+ }
8798
+ if (arg === "--branch" || arg.startsWith("--branch=")) {
8799
+ const { value, nextIndex } = readOptionValue(args, index, "--branch");
8800
+ if (branch && branch !== value) {
8801
+ throw new CommandUsageError(`Conflicting branch values: "${branch}" and "${value}"`);
8802
+ }
8803
+ branch = value.trim();
8804
+ branchFlagUsed = true;
8805
+ index = nextIndex;
8806
+ continue;
8807
+ }
8808
+ if (arg.startsWith("-")) {
8809
+ throw new CommandUsageError(`Unknown option: ${arg}`);
8810
+ }
8811
+ if (branch) {
8812
+ throw new CommandUsageError(`Unexpected argument: ${arg}`);
8813
+ }
8814
+ branch = arg;
8815
+ }
8816
+ if (resume) {
8817
+ if (fromLinearIssueId) {
8818
+ throw new CommandUsageError("Cannot use --resume with --linear <issue-id>");
8819
+ }
8820
+ if (branchFlagUsed) {
8821
+ throw new CommandUsageError("Cannot use --branch with --resume; --resume already names the branch");
8822
+ }
8823
+ if (!resumeBranch)
8824
+ throw new CommandUsageError("--resume requires a branch name");
8825
+ if (branch && branch !== resumeBranch) {
8826
+ throw new CommandUsageError("Cannot pass both a positional branch and --resume");
8827
+ }
8828
+ if (!prompt) {
8829
+ throw new CommandUsageError("--resume requires --prompt; use the dashboard to re-attach without re-prompting");
8830
+ }
8831
+ branch = resumeBranch;
8832
+ }
8833
+ if (branchFlagUsed && !fromLinearIssueId) {
8834
+ throw new CommandUsageError("--branch only applies with --linear; pass the branch as a positional argument otherwise");
8835
+ }
8836
+ if (!resume && !fromLinearIssueId && !prompt) {
8837
+ throw new CommandUsageError("oneshot requires --prompt (or use --linear)");
8838
+ }
8839
+ if (branch)
8840
+ body.branch = branch;
8841
+ if (prompt)
8842
+ body.prompt = prompt;
8843
+ if (Object.keys(envOverrides).length > 0)
8844
+ body.envOverrides = envOverrides;
8845
+ return {
8846
+ branch,
8847
+ prompt,
8848
+ resume,
8849
+ body,
8850
+ keepOpen,
8851
+ fromLinearIssueId,
8852
+ postToLinearTarget
8853
+ };
8854
+ }
8855
+ function timestamp() {
8856
+ const d = new Date;
8857
+ const hh = String(d.getHours()).padStart(2, "0");
8858
+ const mm = String(d.getMinutes()).padStart(2, "0");
8859
+ const ss = String(d.getSeconds()).padStart(2, "0");
8860
+ return `${hh}:${mm}:${ss}`;
8861
+ }
8862
+ function formatLogLine(role, text) {
8863
+ return `[${timestamp()}] [${role}] ${text}`;
8864
+ }
8865
+ function truncateInline(text, limit) {
8866
+ const collapsed = text.replace(/\s+/g, " ").trim();
8867
+ if (collapsed.length <= limit)
8868
+ return collapsed;
8869
+ return `${collapsed.slice(0, limit)}\u2026`;
8870
+ }
8871
+ function summarizeToolInput(toolName, jsonText) {
8872
+ let input = null;
8873
+ try {
8874
+ const parsed = JSON.parse(jsonText);
8875
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
8876
+ input = parsed;
8877
+ }
8878
+ } catch {
8879
+ return truncateInline(jsonText, 100);
8880
+ }
8881
+ if (!input)
8882
+ return truncateInline(jsonText, 100);
8883
+ const keys = TOOL_PRIMARY_KEY[toolName.toLowerCase()];
8884
+ if (keys) {
8885
+ const values = [];
8886
+ for (const key of keys) {
8887
+ const v2 = input[key];
8888
+ if (typeof v2 === "string" && v2.length > 0)
8889
+ values.push(v2);
8890
+ }
8891
+ if (values.length > 0)
8892
+ return truncateInline(values.join(" "), 120);
8893
+ }
8894
+ const parts = [];
8895
+ for (const [key, value] of Object.entries(input)) {
8896
+ if (typeof value === "string")
8897
+ parts.push(`${key}=${truncateInline(value, 40)}`);
8898
+ }
8899
+ if (parts.length === 0)
8900
+ return "";
8901
+ return truncateInline(parts.join(" "), 120);
8902
+ }
8903
+ function summarizeToolResult(text) {
8904
+ const lines = text.split(`
8905
+ `).map((l) => l.trimEnd()).filter((l) => l.length > 0);
8906
+ if (lines.length === 0)
8907
+ return "(empty)";
8908
+ const first = truncateInline(lines[0], 200);
8909
+ return lines.length > 1 ? `${first} (+${lines.length - 1} lines)` : first;
8910
+ }
8911
+ function formatConversationLine(message) {
8912
+ const kind = message.kind ?? "text";
8913
+ if (kind === "toolUse") {
8914
+ const tool = message.toolName ?? "tool";
8915
+ const summary = summarizeToolInput(tool, message.text);
8916
+ return `[${timestamp()}] \u25CF ${tool}(${summary})`;
8917
+ }
8918
+ if (kind === "toolResult") {
8919
+ return `[${timestamp()}] \u23BF ${summarizeToolResult(message.text)}`;
8920
+ }
8921
+ return formatLogLine(message.role, message.text);
8922
+ }
8923
+ function flushStreamingLine(state) {
8924
+ if (state.streamingItemId !== null) {
8925
+ process.stdout.write(`
8926
+ `);
8927
+ state.streamingItemId = null;
8928
+ state.streamingNeedsHeader = false;
8929
+ }
8930
+ }
8931
+ function printNewMessages(state, messages) {
8932
+ for (const message of messages) {
8933
+ if (state.printedMessageIds.has(message.id))
8934
+ continue;
8935
+ if (state.streamingItemId === message.id) {
8936
+ state.printedMessageIds.add(message.id);
8937
+ if (message.status === "completed")
8938
+ flushStreamingLine(state);
8939
+ continue;
8940
+ }
8941
+ flushStreamingLine(state);
8942
+ if (message.text.trim().length === 0) {
8943
+ state.printedMessageIds.add(message.id);
8944
+ continue;
8945
+ }
8946
+ process.stdout.write(`${formatConversationLine(message)}
8947
+ `);
8948
+ state.printedMessageIds.add(message.id);
8949
+ }
8950
+ }
8951
+ function handleConversationEvent(event, state, stderr) {
8952
+ if (event.type === "snapshot") {
8953
+ printNewMessages(state, event.data.conversation.messages);
8954
+ return;
8955
+ }
8956
+ if (event.type === "messageDelta") {
8957
+ if (state.streamingItemId !== event.itemId) {
8958
+ flushStreamingLine(state);
8959
+ state.streamingItemId = event.itemId;
8960
+ state.streamingNeedsHeader = true;
8961
+ }
8962
+ if (state.streamingNeedsHeader) {
8963
+ process.stdout.write(`[${timestamp()}] [assistant] `);
8964
+ state.streamingNeedsHeader = false;
8965
+ }
8966
+ process.stdout.write(event.delta);
8967
+ return;
8968
+ }
8969
+ if (event.type === "error") {
8970
+ flushStreamingLine(state);
8971
+ stderr(`[${timestamp()}] [error] ${event.message}`);
8972
+ return;
8973
+ }
8974
+ }
8975
+ function streamConversation(branch, port, state, stderr, onFatal) {
8976
+ let closed = false;
8977
+ let socket = null;
8978
+ let reconnectTimer = null;
8979
+ let consecutiveFailures = 0;
8980
+ const connect = () => {
8981
+ if (closed)
8982
+ return;
8983
+ const url = `ws://localhost:${port}${apiPaths.streamAgentsWorktreeConversation.replace(":name", encodeURIComponent(branch))}`;
8984
+ const ws = new WebSocket(url);
8985
+ socket = ws;
8986
+ ws.addEventListener("open", () => {
8987
+ consecutiveFailures = 0;
8988
+ });
8989
+ ws.addEventListener("message", (event) => {
8990
+ if (typeof event.data !== "string")
8991
+ return;
8992
+ try {
8993
+ const parsed = AgentsUiConversationEventSchema.parse(JSON.parse(event.data));
8994
+ handleConversationEvent(parsed, state, stderr);
8995
+ } catch {
8996
+ stderr(`[${timestamp()}] [error] received malformed conversation stream data`);
8997
+ }
8998
+ });
8999
+ ws.addEventListener("close", () => {
9000
+ socket = null;
9001
+ if (closed)
9002
+ return;
9003
+ consecutiveFailures += 1;
9004
+ if (RECONNECT_WARN_AT.includes(consecutiveFailures)) {
9005
+ stderr(`[${timestamp()}] [warn] webmux server unreachable, retrying (${consecutiveFailures}/${MAX_CONSECUTIVE_RECONNECTS})`);
9006
+ }
9007
+ if (consecutiveFailures >= MAX_CONSECUTIVE_RECONNECTS) {
9008
+ closed = true;
9009
+ onFatal(`webmux server unreachable after ${consecutiveFailures} reconnect attempts`);
9010
+ return;
9011
+ }
9012
+ reconnectTimer = setTimeout(connect, 2000);
9013
+ });
9014
+ ws.addEventListener("error", () => {});
9015
+ };
9016
+ connect();
9017
+ return {
9018
+ close: () => {
9019
+ closed = true;
9020
+ if (reconnectTimer)
9021
+ clearTimeout(reconnectTimer);
9022
+ if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
9023
+ socket.close();
9024
+ }
9025
+ }
9026
+ };
9027
+ }
9028
+ function recordPrEvents(state, worktree, onPrEvent) {
9029
+ for (const pr of worktree.prs) {
9030
+ if (!state.seenPrUrls.has(pr.url)) {
9031
+ state.seenPrUrls.add(pr.url);
9032
+ onPrEvent(`PR #${pr.number} opened: ${pr.url}`);
9033
+ }
9034
+ if (pr.state === "merged" && !state.seenMergedUrls.has(pr.url)) {
9035
+ state.seenMergedUrls.add(pr.url);
9036
+ onPrEvent(`PR #${pr.number} merged: ${pr.url}`);
9037
+ }
9038
+ }
9039
+ }
9040
+ function pollProjectState(branch, port, state, callbacks, stderr) {
9041
+ const api = createApi(`http://localhost:${port}`);
9042
+ let stopped = false;
9043
+ let timer = null;
9044
+ const forcePrSync = async () => {
9045
+ try {
9046
+ const refreshed = await api.syncWorktreePrs({ params: { name: branch } });
9047
+ recordPrEvents(state, refreshed, callbacks.onPrEvent);
9048
+ } catch (err) {
9049
+ const msg = err instanceof Error ? err.message : String(err);
9050
+ stderr(`[${timestamp()}] [warn] failed to sync PRs from server: ${msg}`);
9051
+ }
9052
+ };
9053
+ const tick = async () => {
9054
+ if (stopped)
9055
+ return;
9056
+ try {
9057
+ const response = await api.fetchWorktrees();
9058
+ const worktree = response.worktrees.find((w3) => w3.branch === branch);
9059
+ if (!worktree) {
9060
+ if (state.hadOpenSession) {
9061
+ callbacks.onWorktreeRemoved();
9062
+ return;
9063
+ }
9064
+ } else {
9065
+ if (worktree.mux) {
9066
+ state.hadOpenSession = true;
9067
+ state.consecutiveClosedReadings = 0;
9068
+ }
9069
+ recordPrEvents(state, worktree, callbacks.onPrEvent);
9070
+ if (worktree.oneshot) {
9071
+ state.watcherWasArmed = true;
9072
+ } else if (state.watcherWasArmed && worktree.mux) {
9073
+ callbacks.onUserTookOver();
9074
+ return;
9075
+ }
9076
+ if (state.hadOpenSession && !worktree.mux) {
9077
+ state.consecutiveClosedReadings += 1;
9078
+ if (state.consecutiveClosedReadings >= 2) {
9079
+ callbacks.onSessionClosed();
9080
+ return;
9081
+ }
9082
+ }
9083
+ const status2 = worktree.status;
9084
+ const isTerminal = status2 === "stopped" || status2 === "error";
9085
+ const isIdle = status2 === "idle";
9086
+ if (isTerminal || isIdle) {
9087
+ if (state.idleSinceMs === null)
9088
+ state.idleSinceMs = Date.now();
9089
+ const isStable = isTerminal || Date.now() - state.idleSinceMs >= IDLE_GRACE_MS;
9090
+ if (isStable) {
9091
+ await forcePrSync();
9092
+ if (state.seenPrUrls.size > 0) {
9093
+ callbacks.onAgentDone(`agent ${status2} after opening PR`);
9094
+ } else {
9095
+ callbacks.onAgentStuck(`agent ${status2} without opening a PR`);
9096
+ }
9097
+ return;
9098
+ }
9099
+ } else {
9100
+ state.idleSinceMs = null;
9101
+ }
9102
+ }
9103
+ } catch {}
9104
+ timer = setTimeout(tick, 3000);
9105
+ };
9106
+ tick();
9107
+ return {
9108
+ stop: () => {
9109
+ stopped = true;
9110
+ if (timer)
9111
+ clearTimeout(timer);
9112
+ }
9113
+ };
9114
+ }
9115
+ async function ensureWorktreeReady(branch, port, stderr) {
9116
+ const api = createApi(`http://localhost:${port}`);
9117
+ const deadline = Date.now() + 60000;
9118
+ while (Date.now() < deadline) {
9119
+ try {
9120
+ const response = await api.fetchWorktrees();
9121
+ const worktree = response.worktrees.find((w3) => w3.branch === branch);
9122
+ if (worktree && worktree.mux && worktree.status !== "creating" && worktree.status !== "closed") {
9123
+ return { ready: true, worktree };
9124
+ }
9125
+ } catch {}
9126
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
9127
+ }
9128
+ stderr(`[${timestamp()}] [error] timed out waiting for ${branch} session to start`);
9129
+ return { ready: false };
9130
+ }
9131
+ function printConversationHistory(initial, state) {
9132
+ printNewMessages(state, initial.conversation.messages);
9133
+ }
9134
+ function pollConversationHistory(branch, port, state) {
9135
+ const api = createApi(`http://localhost:${port}`);
9136
+ let stopped = false;
9137
+ let timer = null;
9138
+ const tick = async () => {
9139
+ if (stopped)
9140
+ return;
9141
+ try {
9142
+ const response = await api.fetchAgentsWorktreeConversationHistory({ params: { name: branch } });
9143
+ printNewMessages(state, response.conversation.messages);
9144
+ } catch {}
9145
+ if (!stopped)
9146
+ timer = setTimeout(tick, 2000);
9147
+ };
9148
+ tick();
9149
+ return {
9150
+ stop: () => {
9151
+ stopped = true;
9152
+ if (timer)
9153
+ clearTimeout(timer);
9154
+ }
9155
+ };
9156
+ }
9157
+ function deriveOneshotIssueTitle(prompt) {
9158
+ if (!prompt)
9159
+ return null;
9160
+ const firstLine = prompt.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
9161
+ if (!firstLine)
9162
+ return null;
9163
+ return firstLine.length > 100 ? `${firstLine.slice(0, 97)}\u2026` : firstLine;
9164
+ }
9165
+ async function runOneshot(parsed, port) {
9166
+ const stdout = (line) => {
9167
+ process.stdout.write(`${line}
9168
+ `);
9169
+ };
9170
+ const stderr = (line) => {
9171
+ process.stderr.write(`${line}
9172
+ `);
9173
+ };
9174
+ const api = createApi(`http://localhost:${port}`);
9175
+ let branch = parsed.branch;
9176
+ const body = { ...parsed.body };
9177
+ let fromLinearIssueId = parsed.fromLinearIssueId;
9178
+ let postToLinearTarget = parsed.postToLinearTarget;
9179
+ try {
9180
+ if (postToLinearTarget) {
9181
+ const availability = await api.fetchLinearIssues();
9182
+ if (availability.availability === "missing_api_key") {
9183
+ stderr(`[${timestamp()}] [error] server has no LINEAR_API_KEY \u2014 the post-back to Linear at the end of the run will fail. Set the env var on the webmux server and restart it.`);
9184
+ return 1;
9185
+ }
9186
+ if (availability.availability === "disabled") {
9187
+ stderr(`[${timestamp()}] [error] Linear integration is disabled on the webmux server.`);
9188
+ return 1;
9189
+ }
9190
+ }
9191
+ if (postToLinearTarget?.kind === "team") {
9192
+ const title = deriveOneshotIssueTitle(parsed.prompt);
9193
+ if (!title) {
9194
+ stderr(`[${timestamp()}] [error] --linear ${postToLinearTarget.teamKey} requires --prompt to derive an issue title`);
9195
+ return 1;
9196
+ }
9197
+ if (parsed.resume) {
9198
+ stdout(`[${timestamp()}] [event] no Linear issue for this resume; creating a fresh ${postToLinearTarget.teamKey}-N for the post-back`);
9199
+ }
9200
+ stdout(`[${timestamp()}] [event] creating Linear issue in team ${postToLinearTarget.teamKey}...`);
9201
+ const team = await fetchTeamByKey(postToLinearTarget.teamKey);
9202
+ if (!team.ok) {
9203
+ stderr(`[${timestamp()}] [error] Linear team lookup failed: ${team.error}`);
9204
+ return 1;
9205
+ }
9206
+ const created = await createLinearIssue({
9207
+ teamId: team.data.id,
9208
+ title,
9209
+ description: ""
9210
+ });
9211
+ if (!created.ok) {
9212
+ stderr(`[${timestamp()}] [error] Linear issue creation failed: ${created.error}`);
9213
+ return 1;
9214
+ }
9215
+ stdout(`[${timestamp()}] [event] created Linear issue ${created.data.identifier} \u2192 ${created.data.url}`);
9216
+ fromLinearIssueId = created.data.identifier;
9217
+ postToLinearTarget = { kind: "issue", issueId: created.data.identifier };
9218
+ }
9219
+ if (fromLinearIssueId) {
9220
+ stdout(`[${timestamp()}] [event] resolving Linear issue ${fromLinearIssueId}...`);
9221
+ const seedResult = await buildSeedFromLinear({ issueId: fromLinearIssueId }, defaultSeedFromLinearDeps);
9222
+ if (!seedResult.ok) {
9223
+ stderr(`[${timestamp()}] [error] Linear seed lookup failed: ${seedResult.error}`);
9224
+ return 1;
9225
+ }
9226
+ const seed = seedResult.data;
9227
+ stdout(`[${timestamp()}] [event] seed source: ${seed.source}${seed.branch ? ` branch=${seed.branch}` : ""}${seed.prUrl ? ` pr=${seed.prUrl}` : ""}`);
9228
+ const resolvedBranch = branch ?? seed.branch ?? null;
9229
+ if (!resolvedBranch) {
9230
+ stderr(`[${timestamp()}] [error] Linear issue did not resolve to a branch; pass --branch to override.`);
9231
+ return 1;
9232
+ }
9233
+ branch = resolvedBranch;
9234
+ body.branch = resolvedBranch;
9235
+ if (seed.source !== "none")
9236
+ body.mode = "existing";
9237
+ body.fromLinear = {
9238
+ issueId: fromLinearIssueId,
9239
+ ...seed.conversationMarkdown ? { conversationContext: seed.conversationMarkdown } : {}
9240
+ };
9241
+ }
9242
+ const existingWorktree = branch ? (await api.fetchWorktrees()).worktrees.find((w3) => w3.branch === branch) : undefined;
9243
+ const oneshotConfig = {
9244
+ autoCloseOnDone: !parsed.keepOpen,
9245
+ ...postToLinearTarget ? { postToLinearOnDone: postToLinearTarget } : {}
9246
+ };
9247
+ if (parsed.resume || existingWorktree) {
9248
+ if (!branch)
9249
+ throw new Error("resume requires a branch name");
9250
+ const reason = parsed.resume ? "resuming" : `worktree exists, resuming ${branch}`;
9251
+ stdout(`[${timestamp()}] [event] ${reason}`);
9252
+ if (fromLinearIssueId) {
9253
+ stdout(`[${timestamp()}] [event] skipping Linear seed \u2014 agent's existing session history already covers it`);
9254
+ }
9255
+ await api.openWorktree({
9256
+ params: { name: branch },
9257
+ body: {
9258
+ ...parsed.prompt ? { prompt: parsed.prompt } : {},
9259
+ oneshot: oneshotConfig
9260
+ }
9261
+ });
9262
+ if (parsed.prompt)
9263
+ stdout(`[${timestamp()}] [event] sent prompt`);
9264
+ } else {
9265
+ stdout(`[${timestamp()}] [event] creating worktree${branch ? ` ${branch}` : ""}...`);
9266
+ const result = await api.createWorktree({
9267
+ body: { ...body, source: "oneshot", oneshot: oneshotConfig }
9268
+ });
9269
+ branch = result.primaryBranch;
9270
+ stdout(`[${timestamp()}] [event] created ${branch}`);
9271
+ }
9272
+ } catch (error) {
9273
+ stderr(`[${timestamp()}] [error] ${formatServerError(error, port)}`);
9274
+ return 1;
9275
+ }
9276
+ if (!branch) {
9277
+ stderr(`[${timestamp()}] [error] could not resolve branch`);
9278
+ return 1;
9279
+ }
9280
+ const ready = await ensureWorktreeReady(branch, port, stderr);
9281
+ if (!ready.ready)
9282
+ return 1;
9283
+ const conversationState = {
9284
+ printedMessageIds: new Set,
9285
+ streamingItemId: null,
9286
+ streamingNeedsHeader: false
9287
+ };
9288
+ try {
9289
+ const initial = await api.fetchAgentsWorktreeConversationHistory({ params: { name: branch } });
9290
+ printConversationHistory(initial, conversationState);
9291
+ } catch {}
9292
+ let resolveExit;
9293
+ const exitPromise = new Promise((resolve3) => {
9294
+ resolveExit = resolve3;
9295
+ });
9296
+ let exiting = false;
9297
+ let stream = null;
9298
+ let historyPoller = null;
9299
+ let poller = null;
9300
+ const finalize = (code) => {
9301
+ if (exiting)
9302
+ return;
9303
+ exiting = true;
9304
+ stream?.close();
9305
+ historyPoller?.stop();
9306
+ poller?.stop();
9307
+ flushStreamingLine(conversationState);
9308
+ resolveExit(code);
9309
+ };
9310
+ stream = streamConversation(branch, port, conversationState, stderr, (reason) => {
9311
+ stderr(`[${timestamp()}] [fatal] ${reason}`);
9312
+ finalize(1);
9313
+ });
9314
+ if (ready.worktree.agentName === "claude") {
9315
+ historyPoller = pollConversationHistory(branch, port, conversationState);
9316
+ }
9317
+ const pollState = {
9318
+ seenPrUrls: new Set,
9319
+ seenMergedUrls: new Set,
9320
+ hadOpenSession: false,
9321
+ consecutiveClosedReadings: 0,
9322
+ idleSinceMs: null,
9323
+ watcherWasArmed: false
9324
+ };
9325
+ poller = pollProjectState(branch, port, pollState, {
9326
+ onSessionClosed: () => {
9327
+ stdout(`[${timestamp()}] [event] session closed \u2014 exiting`);
9328
+ finalize(0);
9329
+ },
9330
+ onWorktreeRemoved: () => {
9331
+ stdout(`[${timestamp()}] [event] worktree removed \u2014 exiting`);
9332
+ finalize(0);
7946
9333
  },
7947
- pullMain: {
7948
- method: "POST",
7949
- path: apiPaths.pullMain,
7950
- body: PullMainRequestSchema,
7951
- responses: {
7952
- 200: PullMainResponseSchema,
7953
- ...commonErrorResponses
7954
- }
9334
+ onPrEvent: (line) => {
9335
+ flushStreamingLine(conversationState);
9336
+ stdout(`[${timestamp()}] [event] ${line}`);
7955
9337
  },
7956
- fetchCiLogs: {
7957
- method: "GET",
7958
- path: apiPaths.fetchCiLogs,
7959
- pathParams: RunIdParamsSchema,
7960
- responses: {
7961
- 200: CiLogsResponseSchema,
7962
- ...commonErrorResponses
7963
- }
9338
+ onAgentDone: (reason) => {
9339
+ flushStreamingLine(conversationState);
9340
+ stdout(`[${timestamp()}] [event] ${reason} \u2014 exiting`);
9341
+ finalize(0);
7964
9342
  },
7965
- dismissNotification: {
7966
- method: "POST",
7967
- path: apiPaths.dismissNotification,
7968
- pathParams: NotificationIdParamsSchema,
7969
- body: c.noBody(),
7970
- responses: {
7971
- 200: OkResponseSchema,
7972
- 400: ErrorResponseSchema,
7973
- 404: ErrorResponseSchema
7974
- }
7975
- }
7976
- }, {
7977
- strictStatusCodes: true
7978
- });
9343
+ onAgentStuck: (reason) => {
9344
+ flushStreamingLine(conversationState);
9345
+ stderr(`[${timestamp()}] [error] ${reason}`);
9346
+ finalize(1);
9347
+ },
9348
+ onUserTookOver: () => {
9349
+ flushStreamingLine(conversationState);
9350
+ stdout(`[${timestamp()}] [event] user took over from the browser \u2014 exiting`);
9351
+ finalize(0);
9352
+ }
9353
+ }, stderr);
9354
+ const onSignal = () => {
9355
+ stdout(`
9356
+ [${timestamp()}] [event] interrupted \u2014 worktree ${branch} keeps running`);
9357
+ stdout(`[${timestamp()}] [event] resume with: webmux oneshot --resume ${branch}`);
9358
+ finalize(130);
9359
+ };
9360
+ process.on("SIGINT", onSignal);
9361
+ process.on("SIGTERM", onSignal);
9362
+ const finalExit = await exitPromise;
9363
+ process.off("SIGINT", onSignal);
9364
+ process.off("SIGTERM", onSignal);
9365
+ return finalExit;
9366
+ }
9367
+ async function runOneshotCommand(args, port) {
9368
+ let parsed;
9369
+ try {
9370
+ parsed = parseOneshotArgs(args);
9371
+ } catch (error) {
9372
+ console.error(error instanceof Error ? error.message : String(error));
9373
+ console.error(getOneshotUsage());
9374
+ return 1;
9375
+ }
9376
+ if (!parsed) {
9377
+ console.log(getOneshotUsage());
9378
+ return 0;
9379
+ }
9380
+ return await runOneshot(parsed, port);
9381
+ }
9382
+ var TOOL_PRIMARY_KEY, MAX_CONSECUTIVE_RECONNECTS = 30, RECONNECT_WARN_AT, IDLE_GRACE_MS = 15000;
9383
+ var init_oneshot = __esm(() => {
9384
+ init_src();
9385
+ init_linear_service();
9386
+ init_conversation_export_service();
9387
+ init_shared();
9388
+ TOOL_PRIMARY_KEY = {
9389
+ bash: ["command"],
9390
+ bashoutput: ["bash_id"],
9391
+ killshell: ["shell_id"],
9392
+ read: ["file_path"],
9393
+ edit: ["file_path"],
9394
+ multiedit: ["file_path"],
9395
+ write: ["file_path"],
9396
+ notebookedit: ["notebook_path"],
9397
+ glob: ["pattern"],
9398
+ grep: ["pattern"],
9399
+ webfetch: ["url"],
9400
+ websearch: ["query"],
9401
+ task: ["description", "subagent_type"],
9402
+ exitplanmode: ["plan"]
9403
+ };
9404
+ RECONNECT_WARN_AT = [3, 15];
7979
9405
  });
7980
9406
 
7981
- // packages/api-contract/src/client.ts
7982
- function createApiClient(baseUrl, options = {}) {
7983
- return initClient(apiContract, {
7984
- baseUrl,
7985
- throwOnUnknownStatus: true,
7986
- baseHeaders: {},
7987
- ...options
7988
- });
7989
- }
7990
- function isRecord2(value) {
7991
- return typeof value === "object" && value !== null;
9407
+ // bin/src/linear-commands.ts
9408
+ var exports_linear_commands = {};
9409
+ __export(exports_linear_commands, {
9410
+ runLinearCommand: () => runLinearCommand,
9411
+ parseLinearTargetArg: () => parseLinearTargetArg,
9412
+ parseLinearArgs: () => parseLinearArgs,
9413
+ getLinearUsage: () => getLinearUsage
9414
+ });
9415
+ function getLinearUsage() {
9416
+ return [
9417
+ "Usage:",
9418
+ " webmux linear post <branch> <team-key> [--title <text>]",
9419
+ "",
9420
+ "Creates a new Linear issue in <team-key> and posts the worktree's conversation",
9421
+ "as a JSON attachment + summary comment.",
9422
+ "",
9423
+ " <team-key> Linear team key (e.g. ENG). A new issue is created in that team.",
9424
+ " --title <text> Override the auto-derived title for the new issue",
9425
+ "",
9426
+ "To post into an existing issue, start the session with `webmux oneshot --linear",
9427
+ "<issue-id>` or `webmux add --from-linear <issue-id>` so the issue is the seed.",
9428
+ "",
9429
+ "Examples:",
9430
+ " webmux linear post feat/foo ENG",
9431
+ ' webmux linear post feat/foo ENG --title "Investigate flaky test"'
9432
+ ].join(`
9433
+ `);
7992
9434
  }
7993
- function isRouteResponse(value) {
7994
- return isRecord2(value) && "status" in value && typeof value.status === "number" && "body" in value;
9435
+ function readOptionValue2(args, index, flag) {
9436
+ const arg = args[index];
9437
+ if (!arg)
9438
+ throw new CommandUsageError(`${flag} requires a value`);
9439
+ const prefix = `${flag}=`;
9440
+ if (arg.startsWith(prefix))
9441
+ return { value: arg.slice(prefix.length), nextIndex: index };
9442
+ const value = args[index + 1];
9443
+ if (value === undefined)
9444
+ throw new CommandUsageError(`${flag} requires a value`);
9445
+ return { value, nextIndex: index + 1 };
7995
9446
  }
7996
- function unwrapResponse(response) {
7997
- if (!isRouteResponse(response)) {
7998
- throw new Error("Malformed API client response");
9447
+ function parseLinearTargetArg(raw) {
9448
+ const target = parseLinearTarget(raw);
9449
+ if (target.kind === "team") {
9450
+ return { kind: "team", teamKey: target.teamKey };
7999
9451
  }
8000
- if (response.status < 200 || response.status >= 300) {
8001
- throw new Error(errorMessageFromResponse(response.body, response.status));
9452
+ if (target.kind === "issue") {
9453
+ throw new CommandUsageError(`Post target must be a team key (e.g. ENG). To post to issue ${target.issueId} as part of a session, use --linear ${target.issueId} on the oneshot/add command (loads issue context and posts back to it).`);
8002
9454
  }
8003
- return response.body;
9455
+ throw new CommandUsageError(`Invalid Linear team key "${target.raw}". Use a team key like ENG.`);
8004
9456
  }
8005
- function errorMessageFromResponse(body, status2) {
8006
- if (typeof body === "string") {
8007
- try {
8008
- const parsed = JSON.parse(body);
8009
- return errorMessageFromResponse(parsed, status2);
8010
- } catch {
8011
- return body.trim() || `HTTP ${status2}`;
8012
- }
8013
- }
8014
- if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
8015
- return body.error;
9457
+ function parseLinearArgs(args) {
9458
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
9459
+ return null;
8016
9460
  }
8017
- return `HTTP ${status2}`;
8018
- }
8019
- function withEncodedPathParams(args) {
8020
- const [first, ...rest] = args;
8021
- if (!first || typeof first !== "object" || !("params" in first) || !first.params || typeof first.params !== "object") {
8022
- return args;
9461
+ const subcommand = args[0];
9462
+ if (subcommand !== "post") {
9463
+ throw new CommandUsageError(`Unknown linear subcommand: ${subcommand}`);
8023
9464
  }
8024
- const encodedParams = Object.fromEntries(Object.entries(first.params).map(([key, value]) => [key, encodeURIComponent(String(value))]));
8025
- return [
8026
- {
8027
- ...first,
8028
- params: encodedParams
8029
- },
8030
- ...rest
8031
- ];
8032
- }
8033
- function wrapRouteCall(routeCall) {
8034
- return async (...args) => unwrapResponse(await routeCall(...withEncodedPathParams(args)));
8035
- }
8036
- function wrapClient(client) {
8037
- return Object.fromEntries(Object.entries(client).map(([key, value]) => {
8038
- if (typeof value === "function") {
8039
- return [key, wrapRouteCall((...args) => Promise.resolve(Reflect.apply(value, undefined, args)))];
9465
+ let branch = null;
9466
+ let targetRaw = null;
9467
+ let titleOverride = null;
9468
+ for (let index = 1;index < args.length; index++) {
9469
+ const arg = args[index];
9470
+ if (!arg)
9471
+ continue;
9472
+ if (arg === "--help" || arg === "-h")
9473
+ return null;
9474
+ if (arg === "--title" || arg.startsWith("--title=")) {
9475
+ const { value, nextIndex } = readOptionValue2(args, index, "--title");
9476
+ titleOverride = value;
9477
+ index = nextIndex;
9478
+ continue;
8040
9479
  }
8041
- if (isRecord2(value)) {
8042
- return [key, wrapClient(value)];
9480
+ if (arg.startsWith("-")) {
9481
+ throw new CommandUsageError(`Unknown option: ${arg}`);
8043
9482
  }
8044
- return [key, value];
8045
- }));
9483
+ if (!branch) {
9484
+ branch = arg;
9485
+ continue;
9486
+ }
9487
+ if (!targetRaw) {
9488
+ targetRaw = arg;
9489
+ continue;
9490
+ }
9491
+ throw new CommandUsageError(`Unexpected argument: ${arg}`);
9492
+ }
9493
+ if (!branch)
9494
+ throw new CommandUsageError("linear post requires a <branch> argument");
9495
+ if (!targetRaw)
9496
+ throw new CommandUsageError("linear post requires a <team-key> argument");
9497
+ const baseTarget = parseLinearTargetArg(targetRaw);
9498
+ const target = baseTarget.kind === "team" && titleOverride ? { kind: "team", teamKey: baseTarget.teamKey, title: titleOverride } : baseTarget;
9499
+ return {
9500
+ subcommand: "post",
9501
+ post: { branch, target, titleOverride }
9502
+ };
8046
9503
  }
8047
- function createApi(baseUrl, options = {}) {
8048
- return wrapClient(createApiClient(baseUrl, options));
9504
+ async function runLinearCommand(args, port) {
9505
+ let parsed;
9506
+ try {
9507
+ parsed = parseLinearArgs(args);
9508
+ } catch (error) {
9509
+ console.error(error instanceof Error ? error.message : String(error));
9510
+ console.error(getLinearUsage());
9511
+ return 1;
9512
+ }
9513
+ if (!parsed) {
9514
+ console.log(getLinearUsage());
9515
+ return 0;
9516
+ }
9517
+ const api = createApi(`http://localhost:${port}`);
9518
+ try {
9519
+ const response = await api.postWorktreeToLinear({
9520
+ params: { name: parsed.post.branch },
9521
+ body: { target: parsed.post.target }
9522
+ });
9523
+ console.log(`Posted to Linear issue: ${response.issueUrl}`);
9524
+ if (response.commentUrl)
9525
+ console.log(`Comment: ${response.commentUrl}`);
9526
+ console.log(`Attachment: ${response.attachmentUrl}`);
9527
+ return 0;
9528
+ } catch (error) {
9529
+ console.error(formatServerError(error, port));
9530
+ return 1;
9531
+ }
8049
9532
  }
8050
- var init_client = __esm(() => {
8051
- init_index_esm();
8052
- init_contract();
8053
- });
8054
-
8055
- // packages/api-contract/src/index.ts
8056
- var init_src = __esm(() => {
8057
- init_contract();
8058
- init_client();
8059
- init_schemas();
9533
+ var init_linear_commands = __esm(() => {
9534
+ init_src();
9535
+ init_shared();
8060
9536
  });
8061
9537
 
8062
9538
  // backend/src/domain/model.ts
@@ -10113,7 +11589,7 @@ var require_merge = __commonJS((exports) => {
10113
11589
 
10114
11590
  // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/addPairToJSMap.js
10115
11591
  var require_addPairToJSMap = __commonJS((exports) => {
10116
- var log = require_log();
11592
+ var log2 = require_log();
10117
11593
  var merge = require_merge();
10118
11594
  var stringify = require_stringify();
10119
11595
  var identity = require_identity();
@@ -10162,7 +11638,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
10162
11638
  let jsonStr = JSON.stringify(strKey);
10163
11639
  if (jsonStr.length > 40)
10164
11640
  jsonStr = jsonStr.substring(0, 36) + '..."';
10165
- log.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
11641
+ log2.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
10166
11642
  ctx.mapKeyWarned = true;
10167
11643
  }
10168
11644
  return strKey;
@@ -11360,13 +12836,13 @@ var require_timestamp = __commonJS((exports) => {
11360
12836
  resolve: (str) => parseSexagesimal(str, false),
11361
12837
  stringify: stringifySexagesimal
11362
12838
  };
11363
- var timestamp = {
12839
+ var timestamp2 = {
11364
12840
  identify: (value) => value instanceof Date,
11365
12841
  default: true,
11366
12842
  tag: "tag:yaml.org,2002:timestamp",
11367
12843
  test: RegExp("^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})" + "(?:" + "(?:t|T|[ \\t]+)" + "([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)" + "(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?" + ")?$"),
11368
12844
  resolve(str) {
11369
- const match = str.match(timestamp.test);
12845
+ const match = str.match(timestamp2.test);
11370
12846
  if (!match)
11371
12847
  throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd");
11372
12848
  const [, year, month, day, hour, minute, second] = match.map(Number);
@@ -11385,7 +12861,7 @@ var require_timestamp = __commonJS((exports) => {
11385
12861
  };
11386
12862
  exports.floatTime = floatTime;
11387
12863
  exports.intTime = intTime;
11388
- exports.timestamp = timestamp;
12864
+ exports.timestamp = timestamp2;
11389
12865
  });
11390
12866
 
11391
12867
  // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/schema.js
@@ -11402,7 +12878,7 @@ var require_schema3 = __commonJS((exports) => {
11402
12878
  var omap = require_omap();
11403
12879
  var pairs = require_pairs();
11404
12880
  var set = require_set();
11405
- var timestamp = require_timestamp();
12881
+ var timestamp2 = require_timestamp();
11406
12882
  var schema = [
11407
12883
  map.map,
11408
12884
  seq.seq,
@@ -11422,9 +12898,9 @@ var require_schema3 = __commonJS((exports) => {
11422
12898
  omap.omap,
11423
12899
  pairs.pairs,
11424
12900
  set.set,
11425
- timestamp.intTime,
11426
- timestamp.floatTime,
11427
- timestamp.timestamp
12901
+ timestamp2.intTime,
12902
+ timestamp2.floatTime,
12903
+ timestamp2.timestamp
11428
12904
  ];
11429
12905
  exports.schema = schema;
11430
12906
  });
@@ -11446,7 +12922,7 @@ var require_tags = __commonJS((exports) => {
11446
12922
  var pairs = require_pairs();
11447
12923
  var schema$2 = require_schema3();
11448
12924
  var set = require_set();
11449
- var timestamp = require_timestamp();
12925
+ var timestamp2 = require_timestamp();
11450
12926
  var schemas2 = new Map([
11451
12927
  ["core", schema.schema],
11452
12928
  ["failsafe", [map.map, seq.seq, string.string]],
@@ -11460,11 +12936,11 @@ var require_tags = __commonJS((exports) => {
11460
12936
  float: float.float,
11461
12937
  floatExp: float.floatExp,
11462
12938
  floatNaN: float.floatNaN,
11463
- floatTime: timestamp.floatTime,
12939
+ floatTime: timestamp2.floatTime,
11464
12940
  int: int.int,
11465
12941
  intHex: int.intHex,
11466
12942
  intOct: int.intOct,
11467
- intTime: timestamp.intTime,
12943
+ intTime: timestamp2.intTime,
11468
12944
  map: map.map,
11469
12945
  merge: merge.merge,
11470
12946
  null: _null.nullTag,
@@ -11472,7 +12948,7 @@ var require_tags = __commonJS((exports) => {
11472
12948
  pairs: pairs.pairs,
11473
12949
  seq: seq.seq,
11474
12950
  set: set.set,
11475
- timestamp: timestamp.timestamp
12951
+ timestamp: timestamp2.timestamp
11476
12952
  };
11477
12953
  var coreKnownTags = {
11478
12954
  "tag:yaml.org,2002:binary": binary.binary,
@@ -11480,7 +12956,7 @@ var require_tags = __commonJS((exports) => {
11480
12956
  "tag:yaml.org,2002:omap": omap.omap,
11481
12957
  "tag:yaml.org,2002:pairs": pairs.pairs,
11482
12958
  "tag:yaml.org,2002:set": set.set,
11483
- "tag:yaml.org,2002:timestamp": timestamp.timestamp
12959
+ "tag:yaml.org,2002:timestamp": timestamp2.timestamp
11484
12960
  };
11485
12961
  function getTags(customTags, schemaName, addMergeTag) {
11486
12962
  const schemaTags = schemas2.get(schemaName);
@@ -12748,9 +14224,9 @@ var require_resolve_block_scalar = __commonJS((exports) => {
12748
14224
  default: {
12749
14225
  const message = `Unexpected token in block scalar header: ${token.type}`;
12750
14226
  onError(token, "UNEXPECTED_TOKEN", message);
12751
- const ts = token.source;
12752
- if (ts && typeof ts === "string")
12753
- length += ts.length;
14227
+ const ts2 = token.source;
14228
+ if (ts2 && typeof ts2 === "string")
14229
+ length += ts2.length;
12754
14230
  }
12755
14231
  }
12756
14232
  }
@@ -13053,9 +14529,9 @@ var require_compose_scalar = __commonJS((exports) => {
13053
14529
  if (schema.compat) {
13054
14530
  const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR];
13055
14531
  if (tag.tag !== compat.tag) {
13056
- const ts = directives.tagString(tag.tag);
14532
+ const ts2 = directives.tagString(tag.tag);
13057
14533
  const cs = directives.tagString(compat.tag);
13058
- const msg = `Value may be parsed as either ${ts} or ${cs}`;
14534
+ const msg = `Value may be parsed as either ${ts2} or ${cs}`;
13059
14535
  onError(token, "TAG_RESOLVE_FAILED", msg, true);
13060
14536
  }
13061
14537
  }
@@ -15319,7 +16795,7 @@ var require_public_api = __commonJS((exports) => {
15319
16795
  var composer = require_composer();
15320
16796
  var Document = require_Document();
15321
16797
  var errors2 = require_errors();
15322
- var log = require_log();
16798
+ var log2 = require_log();
15323
16799
  var identity = require_identity();
15324
16800
  var lineCounter = require_line_counter();
15325
16801
  var parser = require_parser();
@@ -15371,7 +16847,7 @@ var require_public_api = __commonJS((exports) => {
15371
16847
  const doc = parseDocument(src, options);
15372
16848
  if (!doc)
15373
16849
  return null;
15374
- doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
16850
+ doc.warnings.forEach((warning) => log2.warn(doc.options.logLevel, warning));
15375
16851
  if (doc.errors.length > 0) {
15376
16852
  if (doc.options.logLevel !== "silent")
15377
16853
  throw doc.errors[0];
@@ -15460,6 +16936,15 @@ var init_dist5 = __esm(() => {
15460
16936
  // backend/src/adapters/config.ts
15461
16937
  import { readFileSync as readFileSync3 } from "fs";
15462
16938
  import { dirname as dirname2, join as join7, resolve as resolve5 } from "path";
16939
+ function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
16940
+ return [
16941
+ "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.",
16942
+ "Your job is to take the task to its real conclusion without pausing:",
16943
+ "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.",
16944
+ "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.",
16945
+ "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)."
16946
+ ].join(" ");
16947
+ }
15463
16948
  function clonePanes(panes) {
15464
16949
  return panes.map((pane) => ({ ...pane }));
15465
16950
  }
@@ -15636,6 +17121,12 @@ function parseLifecycleHooks(raw) {
15636
17121
  }
15637
17122
  return hooks;
15638
17123
  }
17124
+ function parseOneshot(raw) {
17125
+ if (!isRecord4(raw))
17126
+ return { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() };
17127
+ const systemPrompt = typeof raw.systemPrompt === "string" && raw.systemPrompt.trim() ? raw.systemPrompt.trim() : DEFAULT_ONESHOT_SYSTEM_PROMPT();
17128
+ return { systemPrompt };
17129
+ }
15639
17130
  function parseAutoName(raw) {
15640
17131
  if (!isRecord4(raw))
15641
17132
  return null;
@@ -15708,7 +17199,8 @@ function parseProjectConfig(parsed) {
15708
17199
  }
15709
17200
  },
15710
17201
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
15711
- autoName: parseAutoName(parsed.auto_name)
17202
+ autoName: parseAutoName(parsed.auto_name),
17203
+ oneshot: parseOneshot(parsed.oneshot)
15712
17204
  };
15713
17205
  }
15714
17206
  function defaultConfig() {
@@ -15875,7 +17367,8 @@ var init_config = __esm(() => {
15875
17367
  linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
15876
17368
  },
15877
17369
  lifecycleHooks: {},
15878
- autoName: null
17370
+ autoName: null,
17371
+ oneshot: { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() }
15879
17372
  };
15880
17373
  });
15881
17374
 
@@ -15902,30 +17395,6 @@ var init_control_token = __esm(() => {
15902
17395
  CONTROL_TOKEN_PATH = `${Bun.env.HOME ?? "/root"}/.config/webmux/control-token`;
15903
17396
  });
15904
17397
 
15905
- // backend/src/lib/log.ts
15906
- function ts() {
15907
- return new Date().toISOString().slice(11, 23);
15908
- }
15909
- var DEBUG, log;
15910
- var init_log = __esm(() => {
15911
- DEBUG = Bun.env.WEBMUX_DEBUG === "1";
15912
- log = {
15913
- info(msg) {
15914
- console.log(`[${ts()}] ${msg}`);
15915
- },
15916
- debug(msg) {
15917
- if (DEBUG)
15918
- console.log(`[${ts()}] ${msg}`);
15919
- },
15920
- warn(msg) {
15921
- console.warn(`[${ts()}] ${msg}`);
15922
- },
15923
- error(msg, err) {
15924
- err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
15925
- }
15926
- };
15927
- });
15928
-
15929
17398
  // backend/src/adapters/docker.ts
15930
17399
  import { stat } from "fs/promises";
15931
17400
  async function pathExists(p2) {
@@ -17019,23 +18488,22 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
17019
18488
  return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
17020
18489
  }
17021
18490
  function buildBuiltInAgentInvocation(input) {
18491
+ const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
17022
18492
  if (input.agent === "codex") {
17023
18493
  const hooksFlag = " --enable codex_hooks";
17024
18494
  const yoloFlag2 = input.yolo ? " --yolo" : "";
17025
18495
  if (input.launchMode === "resume") {
17026
- return `codex${hooksFlag}${yoloFlag2} resume --last`;
18496
+ return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
17027
18497
  }
17028
- const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
17029
18498
  if (input.systemPrompt) {
17030
- return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
18499
+ return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
17031
18500
  }
17032
- return `codex${hooksFlag}${yoloFlag2}${promptSuffix2}`;
18501
+ return `codex${hooksFlag}${yoloFlag2}${promptSuffix}`;
17033
18502
  }
17034
18503
  const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
17035
18504
  if (input.launchMode === "resume") {
17036
- return `claude${yoloFlag} --continue`;
18505
+ return `claude${yoloFlag} --continue${promptSuffix}`;
17037
18506
  }
17038
- const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
17039
18507
  if (input.systemPrompt) {
17040
18508
  return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
17041
18509
  }
@@ -17353,7 +18821,9 @@ async function initializeManagedWorktree(opts) {
17353
18821
  agent: opts.agent,
17354
18822
  runtime: opts.runtime,
17355
18823
  startupEnvValues: { ...opts.startupEnvValues ?? {} },
17356
- allocatedPorts: { ...opts.allocatedPorts ?? {} }
18824
+ allocatedPorts: { ...opts.allocatedPorts ?? {} },
18825
+ ...opts.source ? { source: opts.source } : {},
18826
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
17357
18827
  };
17358
18828
  const paths = await ensureWorktreeStorageDirs(opts.gitDir);
17359
18829
  await writeWorktreeMeta(opts.gitDir, meta);
@@ -17406,7 +18876,9 @@ async function createManagedWorktree(opts, deps2 = {}) {
17406
18876
  controlUrl: opts.controlUrl,
17407
18877
  controlToken: opts.controlToken,
17408
18878
  now: opts.now,
17409
- worktreeId: opts.worktreeId
18879
+ worktreeId: opts.worktreeId,
18880
+ ...opts.source ? { source: opts.source } : {},
18881
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
17410
18882
  });
17411
18883
  if (deps2.tmux) {
17412
18884
  sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
@@ -17546,10 +19018,15 @@ class LifecycleService {
17546
19018
  agent: agent.id
17547
19019
  });
17548
19020
  }
17549
- async openWorktree(branch) {
19021
+ async openWorktree(branch, options = {}) {
17550
19022
  try {
17551
19023
  const resolved = await this.resolveExistingWorktree(branch);
17552
- const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
19024
+ let initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
19025
+ if (options.oneshot) {
19026
+ const nextMeta = { ...initialized.meta, oneshot: options.oneshot };
19027
+ await writeWorktreeMeta(initialized.paths.gitDir, nextMeta);
19028
+ initialized = { ...initialized, meta: nextMeta };
19029
+ }
17553
19030
  const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
17554
19031
  const agent = this.resolveAgentDefinition(initialized.meta.agent);
17555
19032
  const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
@@ -17564,7 +19041,8 @@ class LifecycleService {
17564
19041
  agent,
17565
19042
  initialized,
17566
19043
  worktreePath: resolved.entry.path,
17567
- launchMode
19044
+ launchMode,
19045
+ followUpPrompt: options.prompt
17568
19046
  });
17569
19047
  await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
17570
19048
  return {
@@ -17575,6 +19053,20 @@ class LifecycleService {
17575
19053
  throw this.wrapOperationError(error);
17576
19054
  }
17577
19055
  }
19056
+ async disarmOneshot(branch) {
19057
+ let resolved;
19058
+ try {
19059
+ resolved = await this.resolveExistingWorktree(branch);
19060
+ } catch {
19061
+ return false;
19062
+ }
19063
+ if (!resolved.meta?.oneshot)
19064
+ return false;
19065
+ const nextMeta = { ...resolved.meta };
19066
+ delete nextMeta.oneshot;
19067
+ await writeWorktreeMeta(resolved.gitDir, nextMeta);
19068
+ return true;
19069
+ }
17578
19070
  async closeWorktree(branch) {
17579
19071
  try {
17580
19072
  await this.resolveExistingWorktree(branch);
@@ -17754,7 +19246,7 @@ class LifecycleService {
17754
19246
  }
17755
19247
  listProjectWorktrees() {
17756
19248
  const projectRoot2 = resolve8(this.deps.projectRoot);
17757
- return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve8(entry.path) !== projectRoot2);
19249
+ return this.deps.git.listLiveWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve8(entry.path) !== projectRoot2);
17758
19250
  }
17759
19251
  async readManagedMetas() {
17760
19252
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -17861,8 +19353,10 @@ class LifecycleService {
17861
19353
  agent: input.agent,
17862
19354
  initialized: input.initialized,
17863
19355
  worktreePath: input.worktreePath,
17864
- prompt: input.prompt,
19356
+ creationPrompt: input.creationPrompt,
19357
+ followUpPrompt: input.followUpPrompt,
17865
19358
  launchMode: input.launchMode,
19359
+ source: input.source,
17866
19360
  containerName: containerName2
17867
19361
  }));
17868
19362
  return;
@@ -17874,12 +19368,19 @@ class LifecycleService {
17874
19368
  agent: input.agent,
17875
19369
  initialized: input.initialized,
17876
19370
  worktreePath: input.worktreePath,
17877
- prompt: input.prompt,
17878
- launchMode: input.launchMode
19371
+ creationPrompt: input.creationPrompt,
19372
+ followUpPrompt: input.followUpPrompt,
19373
+ launchMode: input.launchMode,
19374
+ source: input.source
17879
19375
  }));
17880
19376
  }
17881
19377
  buildSessionLayout(input) {
17882
- const systemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
19378
+ const baseSystemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
19379
+ const oneshotPrompt = input.launchMode === "fresh" && input.source === "oneshot" ? this.deps.config.oneshot.systemPrompt : undefined;
19380
+ const systemPrompt = baseSystemPrompt && oneshotPrompt ? `${baseSystemPrompt}
19381
+
19382
+ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
19383
+ const prompt = input.launchMode === "resume" ? input.followUpPrompt : input.creationPrompt;
17883
19384
  const containerName2 = input.containerName;
17884
19385
  return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
17885
19386
  repoRoot: this.deps.projectRoot,
@@ -17894,7 +19395,7 @@ class LifecycleService {
17894
19395
  profileName: input.profileName,
17895
19396
  yolo: input.profile.yolo === true,
17896
19397
  systemPrompt,
17897
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
19398
+ prompt,
17898
19399
  launchMode: input.launchMode
17899
19400
  }),
17900
19401
  shell: buildDockerShellCommand(containerName2, input.worktreePath, input.initialized.paths.runtimeEnvPath)
@@ -17908,7 +19409,7 @@ class LifecycleService {
17908
19409
  profileName: input.profileName,
17909
19410
  yolo: input.profile.yolo === true,
17910
19411
  systemPrompt,
17911
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
19412
+ prompt,
17912
19413
  launchMode: input.launchMode
17913
19414
  }),
17914
19415
  shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
@@ -18032,12 +19533,14 @@ class LifecycleService {
18032
19533
  const { profileName, profile } = this.resolveProfile(input.profile);
18033
19534
  const agent = this.resolveAgentDefinition(input.agent);
18034
19535
  const worktreePath = this.resolveWorktreePath(input.branch);
19536
+ const source = input.source ?? "ui";
18035
19537
  const createProgressBase = {
18036
19538
  branch: input.branch,
18037
19539
  ...baseBranch ? { baseBranch } : {},
18038
19540
  path: worktreePath,
18039
19541
  profile: profileName,
18040
- agent: input.agent
19542
+ agent: input.agent,
19543
+ source
18041
19544
  };
18042
19545
  const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
18043
19546
  let initialized = null;
@@ -18062,7 +19565,9 @@ class LifecycleService {
18062
19565
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
18063
19566
  controlUrl: this.controlUrl(profile.runtime),
18064
19567
  controlToken: await this.deps.getControlToken(),
18065
- deleteBranchOnRollback
19568
+ deleteBranchOnRollback,
19569
+ source,
19570
+ ...input.oneshot ? { oneshot: input.oneshot } : {}
18066
19571
  }, {
18067
19572
  git: this.deps.git
18068
19573
  });
@@ -18100,8 +19605,9 @@ class LifecycleService {
18100
19605
  agent,
18101
19606
  initialized,
18102
19607
  worktreePath,
18103
- prompt: input.prompt,
18104
- launchMode: "fresh"
19608
+ creationPrompt: input.prompt,
19609
+ launchMode: "fresh",
19610
+ source
18105
19611
  });
18106
19612
  await this.reportCreateProgress({
18107
19613
  ...createProgressBase,
@@ -18263,6 +19769,8 @@ function makeDefaultState(input) {
18263
19769
  path: input.path,
18264
19770
  profile: input.profile ?? null,
18265
19771
  agentName: input.agentName ?? null,
19772
+ source: input.source ?? "ui",
19773
+ oneshot: input.oneshot ?? null,
18266
19774
  git: {
18267
19775
  exists: true,
18268
19776
  branch: input.branch,
@@ -18312,6 +19820,10 @@ class ProjectRuntime {
18312
19820
  existing.agentName = input.agentName ?? existing.agentName;
18313
19821
  if (input.runtime)
18314
19822
  existing.agent.runtime = input.runtime;
19823
+ if (input.source !== undefined)
19824
+ existing.source = input.source;
19825
+ if (input.oneshot !== undefined)
19826
+ existing.oneshot = input.oneshot;
18315
19827
  existing.git.exists = true;
18316
19828
  existing.git.branch = input.branch;
18317
19829
  existing.session.windowName = buildWorktreeWindowName(input.branch);
@@ -18322,6 +19834,11 @@ class ProjectRuntime {
18322
19834
  this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
18323
19835
  return created;
18324
19836
  }
19837
+ setOneshot(worktreeId, oneshot) {
19838
+ const state = this.requireWorktree(worktreeId);
19839
+ state.oneshot = oneshot;
19840
+ return state;
19841
+ }
18325
19842
  removeWorktree(worktreeId) {
18326
19843
  const state = this.worktrees.get(worktreeId);
18327
19844
  if (!state)
@@ -18367,36 +19884,36 @@ class ProjectRuntime {
18367
19884
  if (event.branch !== state.branch) {
18368
19885
  this.applyBranchChange(state, event.branch);
18369
19886
  }
18370
- const timestamp = isoNow(now);
19887
+ const timestamp2 = isoNow(now);
18371
19888
  switch (event.type) {
18372
19889
  case "agent_stopped":
18373
19890
  state.agent.lifecycle = "stopped";
18374
- state.agent.lastEventAt = timestamp;
19891
+ state.agent.lastEventAt = timestamp2;
18375
19892
  break;
18376
19893
  case "agent_status_changed":
18377
- this.applyStatusChanged(state, event, timestamp);
19894
+ this.applyStatusChanged(state, event, timestamp2);
18378
19895
  break;
18379
19896
  case "runtime_error":
18380
- this.applyRuntimeError(state, event, timestamp);
19897
+ this.applyRuntimeError(state, event, timestamp2);
18381
19898
  break;
18382
19899
  case "pr_opened":
18383
- state.agent.lastEventAt = timestamp;
19900
+ state.agent.lastEventAt = timestamp2;
18384
19901
  break;
18385
19902
  }
18386
19903
  return state;
18387
19904
  }
18388
- applyStatusChanged(state, event, timestamp) {
19905
+ applyStatusChanged(state, event, timestamp2) {
18389
19906
  state.agent.lifecycle = event.lifecycle;
18390
- state.agent.lastEventAt = timestamp;
19907
+ state.agent.lastEventAt = timestamp2;
18391
19908
  if (state.agent.lastStartedAt === null && event.lifecycle === "running") {
18392
- state.agent.lastStartedAt = timestamp;
19909
+ state.agent.lastStartedAt = timestamp2;
18393
19910
  }
18394
19911
  state.agent.lastError = null;
18395
19912
  }
18396
- applyRuntimeError(state, event, timestamp) {
19913
+ applyRuntimeError(state, event, timestamp2) {
18397
19914
  state.agent.lifecycle = "error";
18398
19915
  state.agent.lastError = event.message;
18399
- state.agent.lastEventAt = timestamp;
19916
+ state.agent.lastEventAt = timestamp2;
18400
19917
  }
18401
19918
  applyBranchChange(state, branch) {
18402
19919
  this.reindexBranch(state.branch, branch, state.worktreeId);
@@ -18507,7 +20024,7 @@ class ReconciliationService {
18507
20024
  return await this.inFlight;
18508
20025
  }
18509
20026
  async runReconcile(normalizedRepoRoot) {
18510
- const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
20027
+ const worktrees = this.deps.git.listLiveWorktrees(normalizedRepoRoot);
18511
20028
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
18512
20029
  let windows = [];
18513
20030
  try {
@@ -18533,6 +20050,8 @@ class ReconciliationService {
18533
20050
  profile: meta?.profile ?? null,
18534
20051
  agentName: meta?.agent ?? null,
18535
20052
  runtime: meta?.runtime ?? "host",
20053
+ source: meta?.source ?? "ui",
20054
+ oneshot: meta?.oneshot ?? null,
18536
20055
  git: {
18537
20056
  dirty: gitStatus.dirty,
18538
20057
  aheadCount: gitStatus.aheadCount,
@@ -18565,7 +20084,9 @@ class ReconciliationService {
18565
20084
  path: state.path,
18566
20085
  profile: state.profile,
18567
20086
  agentName: state.agentName,
18568
- runtime: state.runtime
20087
+ runtime: state.runtime,
20088
+ source: state.source,
20089
+ oneshot: state.oneshot
18569
20090
  });
18570
20091
  this.deps.runtime.setGitState(state.worktreeId, {
18571
20092
  exists: true,
@@ -18605,7 +20126,8 @@ class WorktreeCreationTracker {
18605
20126
  path: progress.path,
18606
20127
  profile: progress.profile,
18607
20128
  agentName: progress.agent,
18608
- phase: progress.phase
20129
+ phase: progress.phase,
20130
+ source: progress.source
18609
20131
  };
18610
20132
  this.worktrees.set(progress.branch, next);
18611
20133
  }
@@ -18711,7 +20233,7 @@ function getWorktreeCommandUsage(command) {
18711
20233
  case "add":
18712
20234
  return [
18713
20235
  "Usage:",
18714
- " webmux add [branch] [--existing] [--base <branch>] [--profile <name>] [--agent <id>] [--prompt <text>] [--env KEY=VALUE] [--detach]",
20236
+ " webmux add [branch] [--existing] [--base <branch>] [--profile <name>] [--agent <id>] [--prompt <text>] [--env KEY=VALUE] [--detach] [--from-linear <issue-id>]",
18715
20237
  "",
18716
20238
  "Options:",
18717
20239
  " --existing Use an existing local or remote branch instead of creating a new one",
@@ -18721,6 +20243,8 @@ function getWorktreeCommandUsage(command) {
18721
20243
  " --prompt <text> Initial agent prompt",
18722
20244
  " --env KEY=VALUE Runtime env override (repeatable)",
18723
20245
  " -d, --detach Create worktree without switching to it",
20246
+ " --from-linear ID Bootstrap from a Linear issue \u2014 loads the issue body as",
20247
+ " context, plus any saved webmux session or linked PR",
18724
20248
  " --help Show this help message"
18725
20249
  ].join(`
18726
20250
  `);
@@ -18782,7 +20306,7 @@ function getWorktreeCommandUsage(command) {
18782
20306
  webmux prune`;
18783
20307
  }
18784
20308
  }
18785
- function readOptionValue(args, index, flag) {
20309
+ function readOptionValue3(args, index, flag) {
18786
20310
  const arg = args[index];
18787
20311
  if (!arg) {
18788
20312
  throw new CommandUsageError(`${flag} requires a value`);
@@ -18815,6 +20339,8 @@ function parseAddCommandArgs(args) {
18815
20339
  const envOverrides = {};
18816
20340
  const selectedAgents = [];
18817
20341
  let detach = false;
20342
+ let fromLinearIssueId = null;
20343
+ let branchExplicit = false;
18818
20344
  for (let index = 0;index < args.length; index++) {
18819
20345
  const arg = args[index];
18820
20346
  if (!arg)
@@ -18831,31 +20357,31 @@ function parseAddCommandArgs(args) {
18831
20357
  continue;
18832
20358
  }
18833
20359
  if (arg === "--profile" || arg.startsWith("--profile=")) {
18834
- const { value, nextIndex } = readOptionValue(args, index, "--profile");
20360
+ const { value, nextIndex } = readOptionValue3(args, index, "--profile");
18835
20361
  input.profile = value;
18836
20362
  index = nextIndex;
18837
20363
  continue;
18838
20364
  }
18839
20365
  if (arg === "--base" || arg.startsWith("--base=")) {
18840
- const { value, nextIndex } = readOptionValue(args, index, "--base");
20366
+ const { value, nextIndex } = readOptionValue3(args, index, "--base");
18841
20367
  input.baseBranch = value;
18842
20368
  index = nextIndex;
18843
20369
  continue;
18844
20370
  }
18845
20371
  if (arg === "--agent" || arg.startsWith("--agent=")) {
18846
- const { value, nextIndex } = readOptionValue(args, index, "--agent");
20372
+ const { value, nextIndex } = readOptionValue3(args, index, "--agent");
18847
20373
  selectedAgents.push(parseAgent(value));
18848
20374
  index = nextIndex;
18849
20375
  continue;
18850
20376
  }
18851
20377
  if (arg === "--prompt" || arg.startsWith("--prompt=")) {
18852
- const { value, nextIndex } = readOptionValue(args, index, "--prompt");
20378
+ const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
18853
20379
  input.prompt = value;
18854
20380
  index = nextIndex;
18855
20381
  continue;
18856
20382
  }
18857
20383
  if (arg === "--env" || arg.startsWith("--env=")) {
18858
- const { value, nextIndex } = readOptionValue(args, index, "--env");
20384
+ const { value, nextIndex } = readOptionValue3(args, index, "--env");
18859
20385
  const separatorIndex = value.indexOf("=");
18860
20386
  if (separatorIndex <= 0) {
18861
20387
  throw new CommandUsageError("--env must use KEY=VALUE");
@@ -18864,6 +20390,26 @@ function parseAddCommandArgs(args) {
18864
20390
  index = nextIndex;
18865
20391
  continue;
18866
20392
  }
20393
+ if (arg === "--from-linear" || arg.startsWith("--from-linear=")) {
20394
+ const { value, nextIndex } = readOptionValue3(args, index, "--from-linear");
20395
+ const trimmed = value.trim();
20396
+ if (!/^[A-Z]+-\d+$/.test(trimmed)) {
20397
+ throw new CommandUsageError(`--from-linear expects an issue id like ENG-123 (got "${trimmed}")`);
20398
+ }
20399
+ fromLinearIssueId = trimmed;
20400
+ index = nextIndex;
20401
+ continue;
20402
+ }
20403
+ if (arg === "--branch" || arg.startsWith("--branch=")) {
20404
+ const { value, nextIndex } = readOptionValue3(args, index, "--branch");
20405
+ if (input.branch && input.branch !== value) {
20406
+ throw new CommandUsageError(`Conflicting branch values: "${input.branch}" and "${value}"`);
20407
+ }
20408
+ input.branch = value.trim();
20409
+ branchExplicit = true;
20410
+ index = nextIndex;
20411
+ continue;
20412
+ }
18867
20413
  if (arg.startsWith("-")) {
18868
20414
  throw new CommandUsageError(`Unknown option: ${arg}`);
18869
20415
  }
@@ -18871,6 +20417,7 @@ function parseAddCommandArgs(args) {
18871
20417
  throw new CommandUsageError(`Unexpected argument: ${arg}`);
18872
20418
  }
18873
20419
  input.branch = arg;
20420
+ branchExplicit = true;
18874
20421
  }
18875
20422
  if (selectedAgents.length > 0) {
18876
20423
  input.agents = selectedAgents;
@@ -18878,7 +20425,7 @@ function parseAddCommandArgs(args) {
18878
20425
  if (Object.keys(envOverrides).length > 0) {
18879
20426
  input.envOverrides = envOverrides;
18880
20427
  }
18881
- return { input, detach };
20428
+ return { input, detach, fromLinearIssueId, branchExplicit };
18882
20429
  }
18883
20430
  function parseBranchCommandArgs(args) {
18884
20431
  let branch = null;
@@ -18922,7 +20469,7 @@ function parseLabelCommandArgs(args) {
18922
20469
  if (optionLabel !== null) {
18923
20470
  throw new CommandUsageError("Cannot use --label more than once");
18924
20471
  }
18925
- const { value, nextIndex } = readOptionValue(args, index, "--label");
20472
+ const { value, nextIndex } = readOptionValue3(args, index, "--label");
18926
20473
  optionLabel = value;
18927
20474
  index = nextIndex;
18928
20475
  continue;
@@ -18974,13 +20521,13 @@ function parseSendCommandArgs(args) {
18974
20521
  if (arg === "--prompt" || arg.startsWith("--prompt=")) {
18975
20522
  if (text)
18976
20523
  throw new CommandUsageError("Cannot use --prompt with a positional prompt argument");
18977
- const { value, nextIndex } = readOptionValue(args, index, "--prompt");
20524
+ const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
18978
20525
  text = value;
18979
20526
  index = nextIndex;
18980
20527
  continue;
18981
20528
  }
18982
20529
  if (arg === "--preamble" || arg.startsWith("--preamble=")) {
18983
- const { value, nextIndex } = readOptionValue(args, index, "--preamble");
20530
+ const { value, nextIndex } = readOptionValue3(args, index, "--preamble");
18984
20531
  preamble = value;
18985
20532
  index = nextIndex;
18986
20533
  continue;
@@ -19046,7 +20593,7 @@ function parseListCommandArgs(args) {
19046
20593
  continue;
19047
20594
  }
19048
20595
  if (arg === "--search" || arg.startsWith("--search=")) {
19049
- const { value, nextIndex } = readOptionValue(args, index, "--search");
20596
+ const { value, nextIndex } = readOptionValue3(args, index, "--search");
19050
20597
  search = value;
19051
20598
  index = nextIndex;
19052
20599
  continue;
@@ -19190,6 +20737,31 @@ async function runWorktreeCommand(context, deps2 = {}) {
19190
20737
  stdout(PHASE_LABELS[progress.phase] ?? progress.phase);
19191
20738
  }
19192
20739
  });
20740
+ if (parsed.fromLinearIssueId) {
20741
+ stdout(`Resolving Linear issue ${parsed.fromLinearIssueId}...`);
20742
+ const seed = await buildSeedFromLinear({ issueId: parsed.fromLinearIssueId }, defaultSeedFromLinearDeps);
20743
+ if (!seed.ok) {
20744
+ stderr(`Linear seed lookup failed: ${seed.error}`);
20745
+ return 1;
20746
+ }
20747
+ stdout(`Linear seed source: ${seed.data.source}${seed.data.branch ? ` branch=${seed.data.branch}` : ""}${seed.data.prUrl ? ` pr=${seed.data.prUrl}` : ""}`);
20748
+ if (!parsed.branchExplicit && seed.data.branch) {
20749
+ parsed.input.branch = seed.data.branch;
20750
+ }
20751
+ if (!parsed.input.branch) {
20752
+ stderr("Linear issue did not resolve to a branch; pass --branch to override.");
20753
+ return 1;
20754
+ }
20755
+ if (seed.data.source !== "none")
20756
+ parsed.input.mode = "existing";
20757
+ if (seed.data.conversationMarkdown) {
20758
+ parsed.input.prompt = parsed.input.prompt ? `${seed.data.conversationMarkdown}
20759
+
20760
+ ---
20761
+
20762
+ ${parsed.input.prompt}` : seed.data.conversationMarkdown;
20763
+ }
20764
+ }
19193
20765
  if (!parsed.input.branch && parsed.input.prompt && runtime2.config.autoName) {
19194
20766
  stdout("Generating branch name...");
19195
20767
  }
@@ -19248,23 +20820,13 @@ async function runWorktreeCommand(context, deps2 = {}) {
19248
20820
  return 0;
19249
20821
  }
19250
20822
  const api = createApi(`http://localhost:${context.port}`);
19251
- try {
19252
- await api.sendWorktreePrompt({
19253
- params: { name: parsed.branch },
19254
- body: {
19255
- text: parsed.text,
19256
- ...parsed.preamble ? { preamble: parsed.preamble } : {}
19257
- }
19258
- });
19259
- } catch (error) {
19260
- if (error instanceof Error && error.message.startsWith("HTTP")) {
19261
- throw error;
20823
+ await withServerConnection(context.port, () => api.sendWorktreePrompt({
20824
+ params: { name: parsed.branch },
20825
+ body: {
20826
+ text: parsed.text,
20827
+ ...parsed.preamble ? { preamble: parsed.preamble } : {}
19262
20828
  }
19263
- if (error instanceof Error && !error.message.includes("fetch")) {
19264
- throw error;
19265
- }
19266
- throw new Error(`Could not connect to webmux server on port ${context.port}. Is it running?`);
19267
- }
20829
+ }));
19268
20830
  stdout(`Sent prompt to ${parsed.branch}`);
19269
20831
  return 0;
19270
20832
  }
@@ -19324,10 +20886,12 @@ async function runWorktreeCommand(context, deps2 = {}) {
19324
20886
  return 1;
19325
20887
  }
19326
20888
  }
19327
- var PHASE_LABELS, CommandUsageError;
20889
+ var PHASE_LABELS;
19328
20890
  var init_worktree_commands = __esm(() => {
19329
20891
  init_dist4();
19330
20892
  init_src();
20893
+ init_conversation_export_service();
20894
+ init_shared();
19331
20895
  init_fs();
19332
20896
  init_tmux();
19333
20897
  init_policies();
@@ -19340,18 +20904,16 @@ var init_worktree_commands = __esm(() => {
19340
20904
  starting_session: "Starting session",
19341
20905
  reconciling: "Reconciling"
19342
20906
  };
19343
- CommandUsageError = class CommandUsageError extends Error {
19344
- };
19345
20907
  });
19346
20908
 
19347
20909
  // bin/src/webmux.ts
19348
- import { resolve as resolve11, dirname as dirname6, join as join11 } from "path";
20910
+ import { resolve as resolve11, dirname as dirname6, join as join10 } from "path";
19349
20911
  import { existsSync as existsSync5 } from "fs";
19350
20912
  import { fileURLToPath } from "url";
19351
20913
  // package.json
19352
20914
  var package_default = {
19353
20915
  name: "webmux",
19354
- version: "0.31.1",
20916
+ version: "0.32.0",
19355
20917
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
19356
20918
  type: "module",
19357
20919
  repository: {
@@ -19419,6 +20981,7 @@ Usage:
19419
20981
  webmux service Manage webmux as a system service
19420
20982
  webmux update Update webmux to the latest version
19421
20983
  webmux add Create a worktree using the dashboard lifecycle
20984
+ webmux oneshot Run a worktree start-to-finish, streaming logs to stdout
19422
20985
  webmux list List worktrees and their status
19423
20986
  webmux open Open an existing worktree session
19424
20987
  webmux close Close a worktree session without removing it
@@ -19429,6 +20992,7 @@ Usage:
19429
20992
  webmux merge Merge a worktree into the main branch and remove it
19430
20993
  webmux send Send a prompt to a running worktree agent
19431
20994
  webmux prune Remove all worktrees in the current project
20995
+ webmux linear Post a worktree conversation to a Linear issue/team
19432
20996
  webmux completion Generate shell completion script (bash, zsh)
19433
20997
 
19434
20998
  Options:
@@ -19443,7 +21007,7 @@ Environment:
19443
21007
  `);
19444
21008
  }
19445
21009
  function isRootCommand(value) {
19446
- return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "prune" || value === "completion";
21010
+ return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "prune" || value === "linear" || value === "completion";
19447
21011
  }
19448
21012
  function isServeRootOption(value) {
19449
21013
  return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
@@ -19626,6 +21190,16 @@ async function main(args = process.argv.slice(2)) {
19626
21190
  }
19627
21191
  await loadEnvFile(resolve11(process.cwd(), ".env.local"));
19628
21192
  await loadEnvFile(resolve11(process.cwd(), ".env"));
21193
+ if (parsed.command === "oneshot") {
21194
+ const { runOneshotCommand: runOneshotCommand2 } = await Promise.resolve().then(() => (init_oneshot(), exports_oneshot));
21195
+ const exitCode2 = await runOneshotCommand2(parsed.commandArgs, parsed.port);
21196
+ process.exit(exitCode2);
21197
+ }
21198
+ if (parsed.command === "linear") {
21199
+ const { runLinearCommand: runLinearCommand2 } = await Promise.resolve().then(() => (init_linear_commands(), exports_linear_commands));
21200
+ const exitCode2 = await runLinearCommand2(parsed.commandArgs, parsed.port);
21201
+ process.exit(exitCode2);
21202
+ }
19629
21203
  if (isWorktreeCommand(parsed.command)) {
19630
21204
  const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
19631
21205
  const exitCode2 = await runWorktreeCommand2({
@@ -19674,8 +21248,8 @@ async function main(args = process.argv.slice(2)) {
19674
21248
  }
19675
21249
  process.on("SIGINT", cleanup);
19676
21250
  process.on("SIGTERM", cleanup);
19677
- const backendEntry = join11(PKG_ROOT, "backend", "dist", "server.js");
19678
- const staticDir = join11(PKG_ROOT, "frontend", "dist");
21251
+ const backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
21252
+ const staticDir = join10(PKG_ROOT, "frontend", "dist");
19679
21253
  if (!existsSync5(staticDir)) {
19680
21254
  console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
19681
21255
  process.exit(1);