webmux 0.31.2 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/webmux.js CHANGED
@@ -462,6 +462,7 @@ _webmux() {
462
462
  'service:Manage webmux as a system service'
463
463
  'update:Update webmux to the latest version'
464
464
  'add:Create a worktree'
465
+ 'oneshot:Run a worktree start-to-finish, streaming logs to stdout'
465
466
  'list:List worktrees and their status'
466
467
  'open:Open an existing worktree session'
467
468
  'close:Close a worktree session'
@@ -472,6 +473,7 @@ _webmux() {
472
473
  'merge:Merge a worktree into main'
473
474
  'send:Send a prompt to a running worktree agent'
474
475
  'prune:Remove all worktrees in the current project'
476
+ 'linear:Post a worktree conversation to a Linear issue/team'
475
477
  'completion:Generate shell completion script'
476
478
  )
477
479
 
@@ -490,6 +492,19 @@ _webmux() {
490
492
  fi
491
493
  fi
492
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
+ ;;
493
508
  completion)
494
509
  if (( CURRENT == 3 )); then
495
510
  local -a shells
@@ -519,7 +534,7 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
519
534
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
520
535
 
521
536
  if [[ \${COMP_CWORD} -eq 1 ]]; then
522
- 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}"))
523
538
  return
524
539
  fi
525
540
 
@@ -531,6 +546,15 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
531
546
  COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
532
547
  fi
533
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
+ ;;
534
558
  completion)
535
559
  if [[ \${COMP_CWORD} -eq 2 ]]; then
536
560
  COMPREPLY=($(compgen -W "bash zsh" -- "\${cur}"))
@@ -1837,7 +1861,29 @@ function detectProjectName(gitRoot) {
1837
1861
  }
1838
1862
  return basename2(gitRoot);
1839
1863
  }
1840
- 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
+ });
1841
1887
 
1842
1888
  // bin/src/init-helpers.ts
1843
1889
  import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
@@ -6970,7 +7016,7 @@ var init_zod = __esm(() => {
6970
7016
  init_external();
6971
7017
  });
6972
7018
 
6973
- // 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
6974
7020
  var isZodType = (obj) => {
6975
7021
  return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
6976
7022
  }, isZodObjectStrict = (obj) => {
@@ -7284,7 +7330,15 @@ var init_index_esm = __esm(() => {
7284
7330
  });
7285
7331
 
7286
7332
  // packages/api-contract/src/schemas.ts
7287
- 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;
7288
7342
  var init_schemas = __esm(() => {
7289
7343
  init_zod();
7290
7344
  BooleanLikeSchema = exports_external.union([
@@ -7305,6 +7359,30 @@ var init_schemas = __esm(() => {
7305
7359
  BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
7306
7360
  AgentIdSchema = exports_external.string().trim().min(1);
7307
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
+ });
7308
7386
  AgentCapabilitiesSchema = exports_external.object({
7309
7387
  terminal: exports_external.literal(true),
7310
7388
  inAppChat: exports_external.boolean(),
@@ -7361,6 +7439,7 @@ var init_schemas = __esm(() => {
7361
7439
  BranchListResponseSchema = exports_external.object({
7362
7440
  branches: exports_external.array(AvailableBranchSchema)
7363
7441
  });
7442
+ WorktreeSourceSchema = exports_external.enum(["ui", "oneshot"]);
7364
7443
  CreateWorktreeRequestSchema = exports_external.object({
7365
7444
  mode: WorktreeCreateModeSchema.optional(),
7366
7445
  branch: exports_external.string().optional(),
@@ -7371,7 +7450,14 @@ var init_schemas = __esm(() => {
7371
7450
  prompt: exports_external.string().optional(),
7372
7451
  envOverrides: exports_external.record(exports_external.string()).optional(),
7373
7452
  createLinearTicket: exports_external.literal(true).optional(),
7374
- 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()
7375
7461
  });
7376
7462
  CreateWorktreeResponseSchema = exports_external.object({
7377
7463
  primaryBranch: exports_external.string(),
@@ -7517,7 +7603,9 @@ var init_schemas = __esm(() => {
7517
7603
  services: exports_external.array(ServiceStatusSchema),
7518
7604
  prs: exports_external.array(PrEntrySchema),
7519
7605
  linearIssue: LinkedLinearIssueSchema.nullable(),
7520
- creation: WorktreeCreationStateSchema.nullable()
7606
+ creation: WorktreeCreationStateSchema.nullable(),
7607
+ source: WorktreeSourceSchema,
7608
+ oneshot: OneshotConfigSchema.nullable()
7521
7609
  });
7522
7610
  ProjectSnapshotSchema = exports_external.object({
7523
7611
  project: exports_external.object({
@@ -7566,13 +7654,16 @@ var init_schemas = __esm(() => {
7566
7654
  });
7567
7655
  AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
7568
7656
  AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
7657
+ AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
7569
7658
  AgentsUiConversationMessageSchema = exports_external.object({
7570
7659
  id: exports_external.string(),
7571
7660
  turnId: exports_external.string(),
7572
7661
  role: AgentsUiConversationMessageRoleSchema,
7573
7662
  text: exports_external.string(),
7574
7663
  status: AgentsUiConversationMessageStatusSchema,
7575
- createdAt: exports_external.string().nullable()
7664
+ createdAt: exports_external.string().nullable(),
7665
+ kind: AgentsUiConversationMessageKindSchema.optional(),
7666
+ toolName: exports_external.string().optional()
7576
7667
  });
7577
7668
  AgentsUiConversationStateSchema = exports_external.object({
7578
7669
  provider: WorktreeConversationProviderSchema,
@@ -7701,6 +7792,8 @@ var init_contract = __esm(() => {
7701
7792
  openWorktree: "/api/worktrees/:name/open",
7702
7793
  closeWorktree: "/api/worktrees/:name/close",
7703
7794
  setWorktreeArchived: "/api/worktrees/:name/archive",
7795
+ syncWorktreePrs: "/api/worktrees/:name/sync-prs",
7796
+ postWorktreeToLinear: "/api/worktrees/:name/linear/post",
7704
7797
  setWorktreeLabel: "/api/worktrees/:name/label",
7705
7798
  sendWorktreePrompt: "/api/worktrees/:name/send",
7706
7799
  mergeWorktree: "/api/worktrees/:name/merge",
@@ -7879,7 +7972,7 @@ var init_contract = __esm(() => {
7879
7972
  method: "POST",
7880
7973
  path: apiPaths.openWorktree,
7881
7974
  pathParams: WorktreeNameParamsSchema,
7882
- body: c.noBody(),
7975
+ body: OpenWorktreeRequestSchema,
7883
7976
  responses: {
7884
7977
  200: OkResponseSchema,
7885
7978
  ...commonErrorResponses
@@ -7905,6 +7998,26 @@ var init_contract = __esm(() => {
7905
7998
  ...commonErrorResponses
7906
7999
  }
7907
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
+ },
7908
8021
  setWorktreeLabel: {
7909
8022
  method: "PUT",
7910
8023
  path: apiPaths.setWorktreeLabel,
@@ -7962,128 +8075,1464 @@ var init_contract = __esm(() => {
7962
8075
  ...commonErrorResponses
7963
8076
  }
7964
8077
  },
7965
- setAutoRemoveOnMerge: {
7966
- method: "PUT",
7967
- path: apiPaths.setAutoRemoveOnMerge,
7968
- body: ToggleEnabledRequestSchema,
7969
- responses: {
7970
- 200: EnabledResponseSchema,
7971
- ...commonErrorResponses
7972
- }
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);
7973
9333
  },
7974
- pullMain: {
7975
- method: "POST",
7976
- path: apiPaths.pullMain,
7977
- body: PullMainRequestSchema,
7978
- responses: {
7979
- 200: PullMainResponseSchema,
7980
- ...commonErrorResponses
7981
- }
9334
+ onPrEvent: (line) => {
9335
+ flushStreamingLine(conversationState);
9336
+ stdout(`[${timestamp()}] [event] ${line}`);
7982
9337
  },
7983
- fetchCiLogs: {
7984
- method: "GET",
7985
- path: apiPaths.fetchCiLogs,
7986
- pathParams: RunIdParamsSchema,
7987
- responses: {
7988
- 200: CiLogsResponseSchema,
7989
- ...commonErrorResponses
7990
- }
9338
+ onAgentDone: (reason) => {
9339
+ flushStreamingLine(conversationState);
9340
+ stdout(`[${timestamp()}] [event] ${reason} \u2014 exiting`);
9341
+ finalize(0);
7991
9342
  },
7992
- dismissNotification: {
7993
- method: "POST",
7994
- path: apiPaths.dismissNotification,
7995
- pathParams: NotificationIdParamsSchema,
7996
- body: c.noBody(),
7997
- responses: {
7998
- 200: OkResponseSchema,
7999
- 400: ErrorResponseSchema,
8000
- 404: ErrorResponseSchema
8001
- }
8002
- }
8003
- }, {
8004
- strictStatusCodes: true
8005
- });
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];
8006
9405
  });
8007
9406
 
8008
- // packages/api-contract/src/client.ts
8009
- function createApiClient(baseUrl, options = {}) {
8010
- return initClient(apiContract, {
8011
- baseUrl,
8012
- throwOnUnknownStatus: true,
8013
- baseHeaders: {},
8014
- ...options
8015
- });
8016
- }
8017
- function isRecord2(value) {
8018
- 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
+ `);
8019
9434
  }
8020
- function isRouteResponse(value) {
8021
- 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 };
8022
9446
  }
8023
- function unwrapResponse(response) {
8024
- if (!isRouteResponse(response)) {
8025
- 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 };
8026
9451
  }
8027
- if (response.status < 200 || response.status >= 300) {
8028
- 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).`);
8029
9454
  }
8030
- return response.body;
9455
+ throw new CommandUsageError(`Invalid Linear team key "${target.raw}". Use a team key like ENG.`);
8031
9456
  }
8032
- function errorMessageFromResponse(body, status2) {
8033
- if (typeof body === "string") {
8034
- try {
8035
- const parsed = JSON.parse(body);
8036
- return errorMessageFromResponse(parsed, status2);
8037
- } catch {
8038
- return body.trim() || `HTTP ${status2}`;
8039
- }
8040
- }
8041
- if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
8042
- return body.error;
9457
+ function parseLinearArgs(args) {
9458
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
9459
+ return null;
8043
9460
  }
8044
- return `HTTP ${status2}`;
8045
- }
8046
- function withEncodedPathParams(args) {
8047
- const [first, ...rest] = args;
8048
- if (!first || typeof first !== "object" || !("params" in first) || !first.params || typeof first.params !== "object") {
8049
- return args;
9461
+ const subcommand = args[0];
9462
+ if (subcommand !== "post") {
9463
+ throw new CommandUsageError(`Unknown linear subcommand: ${subcommand}`);
8050
9464
  }
8051
- const encodedParams = Object.fromEntries(Object.entries(first.params).map(([key, value]) => [key, encodeURIComponent(String(value))]));
8052
- return [
8053
- {
8054
- ...first,
8055
- params: encodedParams
8056
- },
8057
- ...rest
8058
- ];
8059
- }
8060
- function wrapRouteCall(routeCall) {
8061
- return async (...args) => unwrapResponse(await routeCall(...withEncodedPathParams(args)));
8062
- }
8063
- function wrapClient(client) {
8064
- return Object.fromEntries(Object.entries(client).map(([key, value]) => {
8065
- if (typeof value === "function") {
8066
- 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;
8067
9479
  }
8068
- if (isRecord2(value)) {
8069
- return [key, wrapClient(value)];
9480
+ if (arg.startsWith("-")) {
9481
+ throw new CommandUsageError(`Unknown option: ${arg}`);
8070
9482
  }
8071
- return [key, value];
8072
- }));
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
+ };
8073
9503
  }
8074
- function createApi(baseUrl, options = {}) {
8075
- 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
+ }
8076
9532
  }
8077
- var init_client = __esm(() => {
8078
- init_index_esm();
8079
- init_contract();
8080
- });
8081
-
8082
- // packages/api-contract/src/index.ts
8083
- var init_src = __esm(() => {
8084
- init_contract();
8085
- init_client();
8086
- init_schemas();
9533
+ var init_linear_commands = __esm(() => {
9534
+ init_src();
9535
+ init_shared();
8087
9536
  });
8088
9537
 
8089
9538
  // backend/src/domain/model.ts
@@ -10140,7 +11589,7 @@ var require_merge = __commonJS((exports) => {
10140
11589
 
10141
11590
  // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/addPairToJSMap.js
10142
11591
  var require_addPairToJSMap = __commonJS((exports) => {
10143
- var log = require_log();
11592
+ var log2 = require_log();
10144
11593
  var merge = require_merge();
10145
11594
  var stringify = require_stringify();
10146
11595
  var identity = require_identity();
@@ -10189,7 +11638,7 @@ var require_addPairToJSMap = __commonJS((exports) => {
10189
11638
  let jsonStr = JSON.stringify(strKey);
10190
11639
  if (jsonStr.length > 40)
10191
11640
  jsonStr = jsonStr.substring(0, 36) + '..."';
10192
- 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.`);
10193
11642
  ctx.mapKeyWarned = true;
10194
11643
  }
10195
11644
  return strKey;
@@ -11387,13 +12836,13 @@ var require_timestamp = __commonJS((exports) => {
11387
12836
  resolve: (str) => parseSexagesimal(str, false),
11388
12837
  stringify: stringifySexagesimal
11389
12838
  };
11390
- var timestamp = {
12839
+ var timestamp2 = {
11391
12840
  identify: (value) => value instanceof Date,
11392
12841
  default: true,
11393
12842
  tag: "tag:yaml.org,2002:timestamp",
11394
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})?))?" + ")?$"),
11395
12844
  resolve(str) {
11396
- const match = str.match(timestamp.test);
12845
+ const match = str.match(timestamp2.test);
11397
12846
  if (!match)
11398
12847
  throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd");
11399
12848
  const [, year, month, day, hour, minute, second] = match.map(Number);
@@ -11412,7 +12861,7 @@ var require_timestamp = __commonJS((exports) => {
11412
12861
  };
11413
12862
  exports.floatTime = floatTime;
11414
12863
  exports.intTime = intTime;
11415
- exports.timestamp = timestamp;
12864
+ exports.timestamp = timestamp2;
11416
12865
  });
11417
12866
 
11418
12867
  // node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/schema/yaml-1.1/schema.js
@@ -11429,7 +12878,7 @@ var require_schema3 = __commonJS((exports) => {
11429
12878
  var omap = require_omap();
11430
12879
  var pairs = require_pairs();
11431
12880
  var set = require_set();
11432
- var timestamp = require_timestamp();
12881
+ var timestamp2 = require_timestamp();
11433
12882
  var schema = [
11434
12883
  map.map,
11435
12884
  seq.seq,
@@ -11449,9 +12898,9 @@ var require_schema3 = __commonJS((exports) => {
11449
12898
  omap.omap,
11450
12899
  pairs.pairs,
11451
12900
  set.set,
11452
- timestamp.intTime,
11453
- timestamp.floatTime,
11454
- timestamp.timestamp
12901
+ timestamp2.intTime,
12902
+ timestamp2.floatTime,
12903
+ timestamp2.timestamp
11455
12904
  ];
11456
12905
  exports.schema = schema;
11457
12906
  });
@@ -11473,7 +12922,7 @@ var require_tags = __commonJS((exports) => {
11473
12922
  var pairs = require_pairs();
11474
12923
  var schema$2 = require_schema3();
11475
12924
  var set = require_set();
11476
- var timestamp = require_timestamp();
12925
+ var timestamp2 = require_timestamp();
11477
12926
  var schemas2 = new Map([
11478
12927
  ["core", schema.schema],
11479
12928
  ["failsafe", [map.map, seq.seq, string.string]],
@@ -11487,11 +12936,11 @@ var require_tags = __commonJS((exports) => {
11487
12936
  float: float.float,
11488
12937
  floatExp: float.floatExp,
11489
12938
  floatNaN: float.floatNaN,
11490
- floatTime: timestamp.floatTime,
12939
+ floatTime: timestamp2.floatTime,
11491
12940
  int: int.int,
11492
12941
  intHex: int.intHex,
11493
12942
  intOct: int.intOct,
11494
- intTime: timestamp.intTime,
12943
+ intTime: timestamp2.intTime,
11495
12944
  map: map.map,
11496
12945
  merge: merge.merge,
11497
12946
  null: _null.nullTag,
@@ -11499,7 +12948,7 @@ var require_tags = __commonJS((exports) => {
11499
12948
  pairs: pairs.pairs,
11500
12949
  seq: seq.seq,
11501
12950
  set: set.set,
11502
- timestamp: timestamp.timestamp
12951
+ timestamp: timestamp2.timestamp
11503
12952
  };
11504
12953
  var coreKnownTags = {
11505
12954
  "tag:yaml.org,2002:binary": binary.binary,
@@ -11507,7 +12956,7 @@ var require_tags = __commonJS((exports) => {
11507
12956
  "tag:yaml.org,2002:omap": omap.omap,
11508
12957
  "tag:yaml.org,2002:pairs": pairs.pairs,
11509
12958
  "tag:yaml.org,2002:set": set.set,
11510
- "tag:yaml.org,2002:timestamp": timestamp.timestamp
12959
+ "tag:yaml.org,2002:timestamp": timestamp2.timestamp
11511
12960
  };
11512
12961
  function getTags(customTags, schemaName, addMergeTag) {
11513
12962
  const schemaTags = schemas2.get(schemaName);
@@ -12775,9 +14224,9 @@ var require_resolve_block_scalar = __commonJS((exports) => {
12775
14224
  default: {
12776
14225
  const message = `Unexpected token in block scalar header: ${token.type}`;
12777
14226
  onError(token, "UNEXPECTED_TOKEN", message);
12778
- const ts = token.source;
12779
- if (ts && typeof ts === "string")
12780
- length += ts.length;
14227
+ const ts2 = token.source;
14228
+ if (ts2 && typeof ts2 === "string")
14229
+ length += ts2.length;
12781
14230
  }
12782
14231
  }
12783
14232
  }
@@ -13080,9 +14529,9 @@ var require_compose_scalar = __commonJS((exports) => {
13080
14529
  if (schema.compat) {
13081
14530
  const compat = schema.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema[identity.SCALAR];
13082
14531
  if (tag.tag !== compat.tag) {
13083
- const ts = directives.tagString(tag.tag);
14532
+ const ts2 = directives.tagString(tag.tag);
13084
14533
  const cs = directives.tagString(compat.tag);
13085
- const msg = `Value may be parsed as either ${ts} or ${cs}`;
14534
+ const msg = `Value may be parsed as either ${ts2} or ${cs}`;
13086
14535
  onError(token, "TAG_RESOLVE_FAILED", msg, true);
13087
14536
  }
13088
14537
  }
@@ -15346,7 +16795,7 @@ var require_public_api = __commonJS((exports) => {
15346
16795
  var composer = require_composer();
15347
16796
  var Document = require_Document();
15348
16797
  var errors2 = require_errors();
15349
- var log = require_log();
16798
+ var log2 = require_log();
15350
16799
  var identity = require_identity();
15351
16800
  var lineCounter = require_line_counter();
15352
16801
  var parser = require_parser();
@@ -15398,7 +16847,7 @@ var require_public_api = __commonJS((exports) => {
15398
16847
  const doc = parseDocument(src, options);
15399
16848
  if (!doc)
15400
16849
  return null;
15401
- doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
16850
+ doc.warnings.forEach((warning) => log2.warn(doc.options.logLevel, warning));
15402
16851
  if (doc.errors.length > 0) {
15403
16852
  if (doc.options.logLevel !== "silent")
15404
16853
  throw doc.errors[0];
@@ -15487,6 +16936,15 @@ var init_dist5 = __esm(() => {
15487
16936
  // backend/src/adapters/config.ts
15488
16937
  import { readFileSync as readFileSync3 } from "fs";
15489
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
+ }
15490
16948
  function clonePanes(panes) {
15491
16949
  return panes.map((pane) => ({ ...pane }));
15492
16950
  }
@@ -15663,6 +17121,12 @@ function parseLifecycleHooks(raw) {
15663
17121
  }
15664
17122
  return hooks;
15665
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
+ }
15666
17130
  function parseAutoName(raw) {
15667
17131
  if (!isRecord4(raw))
15668
17132
  return null;
@@ -15735,7 +17199,8 @@ function parseProjectConfig(parsed) {
15735
17199
  }
15736
17200
  },
15737
17201
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
15738
- autoName: parseAutoName(parsed.auto_name)
17202
+ autoName: parseAutoName(parsed.auto_name),
17203
+ oneshot: parseOneshot(parsed.oneshot)
15739
17204
  };
15740
17205
  }
15741
17206
  function defaultConfig() {
@@ -15902,7 +17367,8 @@ var init_config = __esm(() => {
15902
17367
  linear: { enabled: true, autoCreateWorktrees: false, createTicketOption: false }
15903
17368
  },
15904
17369
  lifecycleHooks: {},
15905
- autoName: null
17370
+ autoName: null,
17371
+ oneshot: { systemPrompt: DEFAULT_ONESHOT_SYSTEM_PROMPT() }
15906
17372
  };
15907
17373
  });
15908
17374
 
@@ -15929,30 +17395,6 @@ var init_control_token = __esm(() => {
15929
17395
  CONTROL_TOKEN_PATH = `${Bun.env.HOME ?? "/root"}/.config/webmux/control-token`;
15930
17396
  });
15931
17397
 
15932
- // backend/src/lib/log.ts
15933
- function ts() {
15934
- return new Date().toISOString().slice(11, 23);
15935
- }
15936
- var DEBUG, log;
15937
- var init_log = __esm(() => {
15938
- DEBUG = Bun.env.WEBMUX_DEBUG === "1";
15939
- log = {
15940
- info(msg) {
15941
- console.log(`[${ts()}] ${msg}`);
15942
- },
15943
- debug(msg) {
15944
- if (DEBUG)
15945
- console.log(`[${ts()}] ${msg}`);
15946
- },
15947
- warn(msg) {
15948
- console.warn(`[${ts()}] ${msg}`);
15949
- },
15950
- error(msg, err) {
15951
- err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
15952
- }
15953
- };
15954
- });
15955
-
15956
17398
  // backend/src/adapters/docker.ts
15957
17399
  import { stat } from "fs/promises";
15958
17400
  async function pathExists(p2) {
@@ -17046,23 +18488,22 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
17046
18488
  return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
17047
18489
  }
17048
18490
  function buildBuiltInAgentInvocation(input) {
18491
+ const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
17049
18492
  if (input.agent === "codex") {
17050
18493
  const hooksFlag = " --enable codex_hooks";
17051
18494
  const yoloFlag2 = input.yolo ? " --yolo" : "";
17052
18495
  if (input.launchMode === "resume") {
17053
- return `codex${hooksFlag}${yoloFlag2} resume --last`;
18496
+ return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
17054
18497
  }
17055
- const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
17056
18498
  if (input.systemPrompt) {
17057
- return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
18499
+ return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
17058
18500
  }
17059
- return `codex${hooksFlag}${yoloFlag2}${promptSuffix2}`;
18501
+ return `codex${hooksFlag}${yoloFlag2}${promptSuffix}`;
17060
18502
  }
17061
18503
  const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
17062
18504
  if (input.launchMode === "resume") {
17063
- return `claude${yoloFlag} --continue`;
18505
+ return `claude${yoloFlag} --continue${promptSuffix}`;
17064
18506
  }
17065
- const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
17066
18507
  if (input.systemPrompt) {
17067
18508
  return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
17068
18509
  }
@@ -17380,7 +18821,9 @@ async function initializeManagedWorktree(opts) {
17380
18821
  agent: opts.agent,
17381
18822
  runtime: opts.runtime,
17382
18823
  startupEnvValues: { ...opts.startupEnvValues ?? {} },
17383
- allocatedPorts: { ...opts.allocatedPorts ?? {} }
18824
+ allocatedPorts: { ...opts.allocatedPorts ?? {} },
18825
+ ...opts.source ? { source: opts.source } : {},
18826
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
17384
18827
  };
17385
18828
  const paths = await ensureWorktreeStorageDirs(opts.gitDir);
17386
18829
  await writeWorktreeMeta(opts.gitDir, meta);
@@ -17433,7 +18876,9 @@ async function createManagedWorktree(opts, deps2 = {}) {
17433
18876
  controlUrl: opts.controlUrl,
17434
18877
  controlToken: opts.controlToken,
17435
18878
  now: opts.now,
17436
- worktreeId: opts.worktreeId
18879
+ worktreeId: opts.worktreeId,
18880
+ ...opts.source ? { source: opts.source } : {},
18881
+ ...opts.oneshot ? { oneshot: opts.oneshot } : {}
17437
18882
  });
17438
18883
  if (deps2.tmux) {
17439
18884
  sessionLayoutPlan = sessionLayoutPlan ?? opts.sessionLayoutPlanBuilder?.(initialized);
@@ -17573,10 +19018,15 @@ class LifecycleService {
17573
19018
  agent: agent.id
17574
19019
  });
17575
19020
  }
17576
- async openWorktree(branch) {
19021
+ async openWorktree(branch, options = {}) {
17577
19022
  try {
17578
19023
  const resolved = await this.resolveExistingWorktree(branch);
17579
- 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
+ }
17580
19030
  const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
17581
19031
  const agent = this.resolveAgentDefinition(initialized.meta.agent);
17582
19032
  const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
@@ -17591,7 +19041,8 @@ class LifecycleService {
17591
19041
  agent,
17592
19042
  initialized,
17593
19043
  worktreePath: resolved.entry.path,
17594
- launchMode
19044
+ launchMode,
19045
+ followUpPrompt: options.prompt
17595
19046
  });
17596
19047
  await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
17597
19048
  return {
@@ -17602,6 +19053,20 @@ class LifecycleService {
17602
19053
  throw this.wrapOperationError(error);
17603
19054
  }
17604
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
+ }
17605
19070
  async closeWorktree(branch) {
17606
19071
  try {
17607
19072
  await this.resolveExistingWorktree(branch);
@@ -17888,8 +19353,10 @@ class LifecycleService {
17888
19353
  agent: input.agent,
17889
19354
  initialized: input.initialized,
17890
19355
  worktreePath: input.worktreePath,
17891
- prompt: input.prompt,
19356
+ creationPrompt: input.creationPrompt,
19357
+ followUpPrompt: input.followUpPrompt,
17892
19358
  launchMode: input.launchMode,
19359
+ source: input.source,
17893
19360
  containerName: containerName2
17894
19361
  }));
17895
19362
  return;
@@ -17901,12 +19368,19 @@ class LifecycleService {
17901
19368
  agent: input.agent,
17902
19369
  initialized: input.initialized,
17903
19370
  worktreePath: input.worktreePath,
17904
- prompt: input.prompt,
17905
- launchMode: input.launchMode
19371
+ creationPrompt: input.creationPrompt,
19372
+ followUpPrompt: input.followUpPrompt,
19373
+ launchMode: input.launchMode,
19374
+ source: input.source
17906
19375
  }));
17907
19376
  }
17908
19377
  buildSessionLayout(input) {
17909
- 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;
17910
19384
  const containerName2 = input.containerName;
17911
19385
  return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
17912
19386
  repoRoot: this.deps.projectRoot,
@@ -17921,7 +19395,7 @@ class LifecycleService {
17921
19395
  profileName: input.profileName,
17922
19396
  yolo: input.profile.yolo === true,
17923
19397
  systemPrompt,
17924
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
19398
+ prompt,
17925
19399
  launchMode: input.launchMode
17926
19400
  }),
17927
19401
  shell: buildDockerShellCommand(containerName2, input.worktreePath, input.initialized.paths.runtimeEnvPath)
@@ -17935,7 +19409,7 @@ class LifecycleService {
17935
19409
  profileName: input.profileName,
17936
19410
  yolo: input.profile.yolo === true,
17937
19411
  systemPrompt,
17938
- prompt: input.launchMode === "fresh" ? input.prompt : undefined,
19412
+ prompt,
17939
19413
  launchMode: input.launchMode
17940
19414
  }),
17941
19415
  shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
@@ -18059,12 +19533,14 @@ class LifecycleService {
18059
19533
  const { profileName, profile } = this.resolveProfile(input.profile);
18060
19534
  const agent = this.resolveAgentDefinition(input.agent);
18061
19535
  const worktreePath = this.resolveWorktreePath(input.branch);
19536
+ const source = input.source ?? "ui";
18062
19537
  const createProgressBase = {
18063
19538
  branch: input.branch,
18064
19539
  ...baseBranch ? { baseBranch } : {},
18065
19540
  path: worktreePath,
18066
19541
  profile: profileName,
18067
- agent: input.agent
19542
+ agent: input.agent,
19543
+ source
18068
19544
  };
18069
19545
  const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
18070
19546
  let initialized = null;
@@ -18089,7 +19565,9 @@ class LifecycleService {
18089
19565
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
18090
19566
  controlUrl: this.controlUrl(profile.runtime),
18091
19567
  controlToken: await this.deps.getControlToken(),
18092
- deleteBranchOnRollback
19568
+ deleteBranchOnRollback,
19569
+ source,
19570
+ ...input.oneshot ? { oneshot: input.oneshot } : {}
18093
19571
  }, {
18094
19572
  git: this.deps.git
18095
19573
  });
@@ -18127,8 +19605,9 @@ class LifecycleService {
18127
19605
  agent,
18128
19606
  initialized,
18129
19607
  worktreePath,
18130
- prompt: input.prompt,
18131
- launchMode: "fresh"
19608
+ creationPrompt: input.prompt,
19609
+ launchMode: "fresh",
19610
+ source
18132
19611
  });
18133
19612
  await this.reportCreateProgress({
18134
19613
  ...createProgressBase,
@@ -18290,6 +19769,8 @@ function makeDefaultState(input) {
18290
19769
  path: input.path,
18291
19770
  profile: input.profile ?? null,
18292
19771
  agentName: input.agentName ?? null,
19772
+ source: input.source ?? "ui",
19773
+ oneshot: input.oneshot ?? null,
18293
19774
  git: {
18294
19775
  exists: true,
18295
19776
  branch: input.branch,
@@ -18339,6 +19820,10 @@ class ProjectRuntime {
18339
19820
  existing.agentName = input.agentName ?? existing.agentName;
18340
19821
  if (input.runtime)
18341
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;
18342
19827
  existing.git.exists = true;
18343
19828
  existing.git.branch = input.branch;
18344
19829
  existing.session.windowName = buildWorktreeWindowName(input.branch);
@@ -18349,6 +19834,11 @@ class ProjectRuntime {
18349
19834
  this.worktreeIdsByBranch.set(input.branch, input.worktreeId);
18350
19835
  return created;
18351
19836
  }
19837
+ setOneshot(worktreeId, oneshot) {
19838
+ const state = this.requireWorktree(worktreeId);
19839
+ state.oneshot = oneshot;
19840
+ return state;
19841
+ }
18352
19842
  removeWorktree(worktreeId) {
18353
19843
  const state = this.worktrees.get(worktreeId);
18354
19844
  if (!state)
@@ -18394,36 +19884,36 @@ class ProjectRuntime {
18394
19884
  if (event.branch !== state.branch) {
18395
19885
  this.applyBranchChange(state, event.branch);
18396
19886
  }
18397
- const timestamp = isoNow(now);
19887
+ const timestamp2 = isoNow(now);
18398
19888
  switch (event.type) {
18399
19889
  case "agent_stopped":
18400
19890
  state.agent.lifecycle = "stopped";
18401
- state.agent.lastEventAt = timestamp;
19891
+ state.agent.lastEventAt = timestamp2;
18402
19892
  break;
18403
19893
  case "agent_status_changed":
18404
- this.applyStatusChanged(state, event, timestamp);
19894
+ this.applyStatusChanged(state, event, timestamp2);
18405
19895
  break;
18406
19896
  case "runtime_error":
18407
- this.applyRuntimeError(state, event, timestamp);
19897
+ this.applyRuntimeError(state, event, timestamp2);
18408
19898
  break;
18409
19899
  case "pr_opened":
18410
- state.agent.lastEventAt = timestamp;
19900
+ state.agent.lastEventAt = timestamp2;
18411
19901
  break;
18412
19902
  }
18413
19903
  return state;
18414
19904
  }
18415
- applyStatusChanged(state, event, timestamp) {
19905
+ applyStatusChanged(state, event, timestamp2) {
18416
19906
  state.agent.lifecycle = event.lifecycle;
18417
- state.agent.lastEventAt = timestamp;
19907
+ state.agent.lastEventAt = timestamp2;
18418
19908
  if (state.agent.lastStartedAt === null && event.lifecycle === "running") {
18419
- state.agent.lastStartedAt = timestamp;
19909
+ state.agent.lastStartedAt = timestamp2;
18420
19910
  }
18421
19911
  state.agent.lastError = null;
18422
19912
  }
18423
- applyRuntimeError(state, event, timestamp) {
19913
+ applyRuntimeError(state, event, timestamp2) {
18424
19914
  state.agent.lifecycle = "error";
18425
19915
  state.agent.lastError = event.message;
18426
- state.agent.lastEventAt = timestamp;
19916
+ state.agent.lastEventAt = timestamp2;
18427
19917
  }
18428
19918
  applyBranchChange(state, branch) {
18429
19919
  this.reindexBranch(state.branch, branch, state.worktreeId);
@@ -18560,6 +20050,8 @@ class ReconciliationService {
18560
20050
  profile: meta?.profile ?? null,
18561
20051
  agentName: meta?.agent ?? null,
18562
20052
  runtime: meta?.runtime ?? "host",
20053
+ source: meta?.source ?? "ui",
20054
+ oneshot: meta?.oneshot ?? null,
18563
20055
  git: {
18564
20056
  dirty: gitStatus.dirty,
18565
20057
  aheadCount: gitStatus.aheadCount,
@@ -18592,7 +20084,9 @@ class ReconciliationService {
18592
20084
  path: state.path,
18593
20085
  profile: state.profile,
18594
20086
  agentName: state.agentName,
18595
- runtime: state.runtime
20087
+ runtime: state.runtime,
20088
+ source: state.source,
20089
+ oneshot: state.oneshot
18596
20090
  });
18597
20091
  this.deps.runtime.setGitState(state.worktreeId, {
18598
20092
  exists: true,
@@ -18632,7 +20126,8 @@ class WorktreeCreationTracker {
18632
20126
  path: progress.path,
18633
20127
  profile: progress.profile,
18634
20128
  agentName: progress.agent,
18635
- phase: progress.phase
20129
+ phase: progress.phase,
20130
+ source: progress.source
18636
20131
  };
18637
20132
  this.worktrees.set(progress.branch, next);
18638
20133
  }
@@ -18738,7 +20233,7 @@ function getWorktreeCommandUsage(command) {
18738
20233
  case "add":
18739
20234
  return [
18740
20235
  "Usage:",
18741
- " 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>]",
18742
20237
  "",
18743
20238
  "Options:",
18744
20239
  " --existing Use an existing local or remote branch instead of creating a new one",
@@ -18748,6 +20243,8 @@ function getWorktreeCommandUsage(command) {
18748
20243
  " --prompt <text> Initial agent prompt",
18749
20244
  " --env KEY=VALUE Runtime env override (repeatable)",
18750
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",
18751
20248
  " --help Show this help message"
18752
20249
  ].join(`
18753
20250
  `);
@@ -18809,7 +20306,7 @@ function getWorktreeCommandUsage(command) {
18809
20306
  webmux prune`;
18810
20307
  }
18811
20308
  }
18812
- function readOptionValue(args, index, flag) {
20309
+ function readOptionValue3(args, index, flag) {
18813
20310
  const arg = args[index];
18814
20311
  if (!arg) {
18815
20312
  throw new CommandUsageError(`${flag} requires a value`);
@@ -18842,6 +20339,8 @@ function parseAddCommandArgs(args) {
18842
20339
  const envOverrides = {};
18843
20340
  const selectedAgents = [];
18844
20341
  let detach = false;
20342
+ let fromLinearIssueId = null;
20343
+ let branchExplicit = false;
18845
20344
  for (let index = 0;index < args.length; index++) {
18846
20345
  const arg = args[index];
18847
20346
  if (!arg)
@@ -18858,31 +20357,31 @@ function parseAddCommandArgs(args) {
18858
20357
  continue;
18859
20358
  }
18860
20359
  if (arg === "--profile" || arg.startsWith("--profile=")) {
18861
- const { value, nextIndex } = readOptionValue(args, index, "--profile");
20360
+ const { value, nextIndex } = readOptionValue3(args, index, "--profile");
18862
20361
  input.profile = value;
18863
20362
  index = nextIndex;
18864
20363
  continue;
18865
20364
  }
18866
20365
  if (arg === "--base" || arg.startsWith("--base=")) {
18867
- const { value, nextIndex } = readOptionValue(args, index, "--base");
20366
+ const { value, nextIndex } = readOptionValue3(args, index, "--base");
18868
20367
  input.baseBranch = value;
18869
20368
  index = nextIndex;
18870
20369
  continue;
18871
20370
  }
18872
20371
  if (arg === "--agent" || arg.startsWith("--agent=")) {
18873
- const { value, nextIndex } = readOptionValue(args, index, "--agent");
20372
+ const { value, nextIndex } = readOptionValue3(args, index, "--agent");
18874
20373
  selectedAgents.push(parseAgent(value));
18875
20374
  index = nextIndex;
18876
20375
  continue;
18877
20376
  }
18878
20377
  if (arg === "--prompt" || arg.startsWith("--prompt=")) {
18879
- const { value, nextIndex } = readOptionValue(args, index, "--prompt");
20378
+ const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
18880
20379
  input.prompt = value;
18881
20380
  index = nextIndex;
18882
20381
  continue;
18883
20382
  }
18884
20383
  if (arg === "--env" || arg.startsWith("--env=")) {
18885
- const { value, nextIndex } = readOptionValue(args, index, "--env");
20384
+ const { value, nextIndex } = readOptionValue3(args, index, "--env");
18886
20385
  const separatorIndex = value.indexOf("=");
18887
20386
  if (separatorIndex <= 0) {
18888
20387
  throw new CommandUsageError("--env must use KEY=VALUE");
@@ -18891,6 +20390,26 @@ function parseAddCommandArgs(args) {
18891
20390
  index = nextIndex;
18892
20391
  continue;
18893
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
+ }
18894
20413
  if (arg.startsWith("-")) {
18895
20414
  throw new CommandUsageError(`Unknown option: ${arg}`);
18896
20415
  }
@@ -18898,6 +20417,7 @@ function parseAddCommandArgs(args) {
18898
20417
  throw new CommandUsageError(`Unexpected argument: ${arg}`);
18899
20418
  }
18900
20419
  input.branch = arg;
20420
+ branchExplicit = true;
18901
20421
  }
18902
20422
  if (selectedAgents.length > 0) {
18903
20423
  input.agents = selectedAgents;
@@ -18905,7 +20425,7 @@ function parseAddCommandArgs(args) {
18905
20425
  if (Object.keys(envOverrides).length > 0) {
18906
20426
  input.envOverrides = envOverrides;
18907
20427
  }
18908
- return { input, detach };
20428
+ return { input, detach, fromLinearIssueId, branchExplicit };
18909
20429
  }
18910
20430
  function parseBranchCommandArgs(args) {
18911
20431
  let branch = null;
@@ -18949,7 +20469,7 @@ function parseLabelCommandArgs(args) {
18949
20469
  if (optionLabel !== null) {
18950
20470
  throw new CommandUsageError("Cannot use --label more than once");
18951
20471
  }
18952
- const { value, nextIndex } = readOptionValue(args, index, "--label");
20472
+ const { value, nextIndex } = readOptionValue3(args, index, "--label");
18953
20473
  optionLabel = value;
18954
20474
  index = nextIndex;
18955
20475
  continue;
@@ -19001,13 +20521,13 @@ function parseSendCommandArgs(args) {
19001
20521
  if (arg === "--prompt" || arg.startsWith("--prompt=")) {
19002
20522
  if (text)
19003
20523
  throw new CommandUsageError("Cannot use --prompt with a positional prompt argument");
19004
- const { value, nextIndex } = readOptionValue(args, index, "--prompt");
20524
+ const { value, nextIndex } = readOptionValue3(args, index, "--prompt");
19005
20525
  text = value;
19006
20526
  index = nextIndex;
19007
20527
  continue;
19008
20528
  }
19009
20529
  if (arg === "--preamble" || arg.startsWith("--preamble=")) {
19010
- const { value, nextIndex } = readOptionValue(args, index, "--preamble");
20530
+ const { value, nextIndex } = readOptionValue3(args, index, "--preamble");
19011
20531
  preamble = value;
19012
20532
  index = nextIndex;
19013
20533
  continue;
@@ -19073,7 +20593,7 @@ function parseListCommandArgs(args) {
19073
20593
  continue;
19074
20594
  }
19075
20595
  if (arg === "--search" || arg.startsWith("--search=")) {
19076
- const { value, nextIndex } = readOptionValue(args, index, "--search");
20596
+ const { value, nextIndex } = readOptionValue3(args, index, "--search");
19077
20597
  search = value;
19078
20598
  index = nextIndex;
19079
20599
  continue;
@@ -19217,6 +20737,31 @@ async function runWorktreeCommand(context, deps2 = {}) {
19217
20737
  stdout(PHASE_LABELS[progress.phase] ?? progress.phase);
19218
20738
  }
19219
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
+ }
19220
20765
  if (!parsed.input.branch && parsed.input.prompt && runtime2.config.autoName) {
19221
20766
  stdout("Generating branch name...");
19222
20767
  }
@@ -19275,23 +20820,13 @@ async function runWorktreeCommand(context, deps2 = {}) {
19275
20820
  return 0;
19276
20821
  }
19277
20822
  const api = createApi(`http://localhost:${context.port}`);
19278
- try {
19279
- await api.sendWorktreePrompt({
19280
- params: { name: parsed.branch },
19281
- body: {
19282
- text: parsed.text,
19283
- ...parsed.preamble ? { preamble: parsed.preamble } : {}
19284
- }
19285
- });
19286
- } catch (error) {
19287
- if (error instanceof Error && error.message.startsWith("HTTP")) {
19288
- 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 } : {}
19289
20828
  }
19290
- if (error instanceof Error && !error.message.includes("fetch")) {
19291
- throw error;
19292
- }
19293
- throw new Error(`Could not connect to webmux server on port ${context.port}. Is it running?`);
19294
- }
20829
+ }));
19295
20830
  stdout(`Sent prompt to ${parsed.branch}`);
19296
20831
  return 0;
19297
20832
  }
@@ -19351,10 +20886,12 @@ async function runWorktreeCommand(context, deps2 = {}) {
19351
20886
  return 1;
19352
20887
  }
19353
20888
  }
19354
- var PHASE_LABELS, CommandUsageError;
20889
+ var PHASE_LABELS;
19355
20890
  var init_worktree_commands = __esm(() => {
19356
20891
  init_dist4();
19357
20892
  init_src();
20893
+ init_conversation_export_service();
20894
+ init_shared();
19358
20895
  init_fs();
19359
20896
  init_tmux();
19360
20897
  init_policies();
@@ -19367,18 +20904,16 @@ var init_worktree_commands = __esm(() => {
19367
20904
  starting_session: "Starting session",
19368
20905
  reconciling: "Reconciling"
19369
20906
  };
19370
- CommandUsageError = class CommandUsageError extends Error {
19371
- };
19372
20907
  });
19373
20908
 
19374
20909
  // bin/src/webmux.ts
19375
- 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";
19376
20911
  import { existsSync as existsSync5 } from "fs";
19377
20912
  import { fileURLToPath } from "url";
19378
20913
  // package.json
19379
20914
  var package_default = {
19380
20915
  name: "webmux",
19381
- version: "0.31.2",
20916
+ version: "0.32.0",
19382
20917
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
19383
20918
  type: "module",
19384
20919
  repository: {
@@ -19446,6 +20981,7 @@ Usage:
19446
20981
  webmux service Manage webmux as a system service
19447
20982
  webmux update Update webmux to the latest version
19448
20983
  webmux add Create a worktree using the dashboard lifecycle
20984
+ webmux oneshot Run a worktree start-to-finish, streaming logs to stdout
19449
20985
  webmux list List worktrees and their status
19450
20986
  webmux open Open an existing worktree session
19451
20987
  webmux close Close a worktree session without removing it
@@ -19456,6 +20992,7 @@ Usage:
19456
20992
  webmux merge Merge a worktree into the main branch and remove it
19457
20993
  webmux send Send a prompt to a running worktree agent
19458
20994
  webmux prune Remove all worktrees in the current project
20995
+ webmux linear Post a worktree conversation to a Linear issue/team
19459
20996
  webmux completion Generate shell completion script (bash, zsh)
19460
20997
 
19461
20998
  Options:
@@ -19470,7 +21007,7 @@ Environment:
19470
21007
  `);
19471
21008
  }
19472
21009
  function isRootCommand(value) {
19473
- 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";
19474
21011
  }
19475
21012
  function isServeRootOption(value) {
19476
21013
  return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
@@ -19653,6 +21190,16 @@ async function main(args = process.argv.slice(2)) {
19653
21190
  }
19654
21191
  await loadEnvFile(resolve11(process.cwd(), ".env.local"));
19655
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
+ }
19656
21203
  if (isWorktreeCommand(parsed.command)) {
19657
21204
  const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
19658
21205
  const exitCode2 = await runWorktreeCommand2({
@@ -19701,8 +21248,8 @@ async function main(args = process.argv.slice(2)) {
19701
21248
  }
19702
21249
  process.on("SIGINT", cleanup);
19703
21250
  process.on("SIGTERM", cleanup);
19704
- const backendEntry = join11(PKG_ROOT, "backend", "dist", "server.js");
19705
- 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");
19706
21253
  if (!existsSync5(staticDir)) {
19707
21254
  console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
19708
21255
  process.exit(1);