webmux 0.28.1 → 0.30.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
@@ -7190,7 +7190,7 @@ var init_index_esm = __esm(() => {
7190
7190
  });
7191
7191
 
7192
7192
  // packages/api-contract/src/schemas.ts
7193
- var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, AgentKindSchema, CreateWorktreeAgentSelectionSchema, WorktreeCreateModeSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, CreateWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, 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, RunIdParamsSchema;
7193
+ var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, CreateWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, 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;
7194
7194
  var init_schemas = __esm(() => {
7195
7195
  init_zod();
7196
7196
  BooleanLikeSchema = exports_external.union([
@@ -7208,9 +7208,45 @@ var init_schemas = __esm(() => {
7208
7208
  ok: exports_external.literal(true),
7209
7209
  enabled: exports_external.boolean()
7210
7210
  });
7211
- AgentKindSchema = exports_external.enum(["claude", "codex"]);
7212
- CreateWorktreeAgentSelectionSchema = exports_external.enum(["claude", "codex", "both"]);
7211
+ BuiltInAgentIdSchema = exports_external.enum(["claude", "codex"]);
7212
+ AgentIdSchema = exports_external.string().trim().min(1);
7213
7213
  WorktreeCreateModeSchema = exports_external.enum(["new", "existing"]);
7214
+ AgentCapabilitiesSchema = exports_external.object({
7215
+ terminal: exports_external.literal(true),
7216
+ inAppChat: exports_external.boolean(),
7217
+ conversationHistory: exports_external.boolean(),
7218
+ interrupt: exports_external.boolean(),
7219
+ resume: exports_external.boolean()
7220
+ });
7221
+ AgentSummarySchema = exports_external.object({
7222
+ id: AgentIdSchema,
7223
+ label: exports_external.string(),
7224
+ kind: exports_external.enum(["builtin", "custom"]),
7225
+ capabilities: AgentCapabilitiesSchema
7226
+ });
7227
+ AgentDetailsSchema = exports_external.object({
7228
+ id: AgentIdSchema,
7229
+ label: exports_external.string(),
7230
+ kind: exports_external.enum(["builtin", "custom"]),
7231
+ capabilities: AgentCapabilitiesSchema,
7232
+ startCommand: exports_external.string().nullable(),
7233
+ resumeCommand: exports_external.string().nullable()
7234
+ });
7235
+ AgentListResponseSchema = exports_external.object({
7236
+ agents: exports_external.array(AgentDetailsSchema)
7237
+ });
7238
+ UpsertCustomAgentRequestSchema = exports_external.object({
7239
+ label: exports_external.string().trim().min(1),
7240
+ startCommand: exports_external.string().trim().min(1),
7241
+ resumeCommand: exports_external.string().trim().optional()
7242
+ });
7243
+ AgentResponseSchema = exports_external.object({
7244
+ agent: AgentDetailsSchema
7245
+ });
7246
+ ValidateCustomAgentResponseSchema = exports_external.object({
7247
+ normalizedId: AgentIdSchema,
7248
+ warnings: exports_external.array(exports_external.string())
7249
+ });
7214
7250
  WorktreeCreationPhaseSchema = exports_external.enum([
7215
7251
  "creating_worktree",
7216
7252
  "preparing_runtime",
@@ -7236,7 +7272,8 @@ var init_schemas = __esm(() => {
7236
7272
  branch: exports_external.string().optional(),
7237
7273
  baseBranch: exports_external.string().optional(),
7238
7274
  profile: exports_external.string().optional(),
7239
- agent: CreateWorktreeAgentSelectionSchema.optional(),
7275
+ agent: AgentIdSchema.optional(),
7276
+ agents: exports_external.array(AgentIdSchema).min(1).optional(),
7240
7277
  prompt: exports_external.string().optional(),
7241
7278
  envOverrides: exports_external.record(exports_external.string()).optional(),
7242
7279
  createLinearTicket: exports_external.literal(true).optional(),
@@ -7367,7 +7404,8 @@ var init_schemas = __esm(() => {
7367
7404
  dir: exports_external.string(),
7368
7405
  archived: exports_external.boolean(),
7369
7406
  profile: exports_external.string().nullable(),
7370
- agentName: AgentKindSchema.nullable(),
7407
+ agentName: AgentIdSchema.nullable(),
7408
+ agentLabel: exports_external.string().nullable(),
7371
7409
  mux: exports_external.boolean(),
7372
7410
  dirty: exports_external.boolean(),
7373
7411
  unpushed: exports_external.boolean(),
@@ -7412,7 +7450,8 @@ var init_schemas = __esm(() => {
7412
7450
  path: exports_external.string(),
7413
7451
  archived: exports_external.boolean(),
7414
7452
  profile: exports_external.string().nullable(),
7415
- agentName: AgentKindSchema.nullable(),
7453
+ agentName: AgentIdSchema.nullable(),
7454
+ agentLabel: exports_external.string().nullable(),
7416
7455
  mux: exports_external.boolean(),
7417
7456
  status: exports_external.string(),
7418
7457
  dirty: exports_external.boolean(),
@@ -7504,7 +7543,9 @@ var init_schemas = __esm(() => {
7504
7543
  name: exports_external.string(),
7505
7544
  services: exports_external.array(ServiceConfigSchema),
7506
7545
  profiles: exports_external.array(ProfileConfigSchema),
7546
+ agents: exports_external.array(AgentSummarySchema),
7507
7547
  defaultProfileName: exports_external.string(),
7548
+ defaultAgentId: BuiltInAgentIdSchema,
7508
7549
  autoName: exports_external.boolean(),
7509
7550
  linearCreateTicketOption: exports_external.boolean(),
7510
7551
  startupEnvs: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
@@ -7523,6 +7564,9 @@ var init_schemas = __esm(() => {
7523
7564
  NotificationIdParamsSchema = exports_external.object({
7524
7565
  id: NumberLikePathParamSchema
7525
7566
  });
7567
+ AgentIdParamsSchema = exports_external.object({
7568
+ id: AgentIdSchema
7569
+ });
7526
7570
  RunIdParamsSchema = exports_external.object({
7527
7571
  runId: NumberLikePathParamSchema
7528
7572
  });
@@ -7539,6 +7583,11 @@ var init_contract = __esm(() => {
7539
7583
  fetchAvailableBranches: "/api/branches",
7540
7584
  fetchBaseBranches: "/api/base-branches",
7541
7585
  fetchProject: "/api/project",
7586
+ fetchAgents: "/api/agents",
7587
+ createAgent: "/api/agents",
7588
+ updateAgent: "/api/agents/:id",
7589
+ deleteAgent: "/api/agents/:id",
7590
+ validateAgent: "/api/agents/validate",
7542
7591
  attachAgentsWorktreeConversation: "/api/agents/worktrees/:name/attach",
7543
7592
  fetchAgentsWorktreeConversationHistory: "/api/agents/worktrees/:name/history",
7544
7593
  sendAgentsWorktreeConversationMessage: "/api/agents/worktrees/:name/messages",
@@ -7603,6 +7652,60 @@ var init_contract = __esm(() => {
7603
7652
  502: ErrorResponseSchema
7604
7653
  }
7605
7654
  },
7655
+ fetchAgents: {
7656
+ method: "GET",
7657
+ path: apiPaths.fetchAgents,
7658
+ responses: {
7659
+ 200: AgentListResponseSchema,
7660
+ 500: ErrorResponseSchema
7661
+ }
7662
+ },
7663
+ createAgent: {
7664
+ method: "POST",
7665
+ path: apiPaths.createAgent,
7666
+ body: UpsertCustomAgentRequestSchema,
7667
+ responses: {
7668
+ 200: AgentResponseSchema,
7669
+ 400: ErrorResponseSchema,
7670
+ 409: ErrorResponseSchema,
7671
+ 500: ErrorResponseSchema
7672
+ }
7673
+ },
7674
+ updateAgent: {
7675
+ method: "PUT",
7676
+ path: apiPaths.updateAgent,
7677
+ pathParams: AgentIdParamsSchema,
7678
+ body: UpsertCustomAgentRequestSchema,
7679
+ responses: {
7680
+ 200: AgentResponseSchema,
7681
+ 400: ErrorResponseSchema,
7682
+ 404: ErrorResponseSchema,
7683
+ 409: ErrorResponseSchema,
7684
+ 500: ErrorResponseSchema
7685
+ }
7686
+ },
7687
+ deleteAgent: {
7688
+ method: "DELETE",
7689
+ path: apiPaths.deleteAgent,
7690
+ pathParams: AgentIdParamsSchema,
7691
+ body: c2.noBody(),
7692
+ responses: {
7693
+ 200: OkResponseSchema,
7694
+ 400: ErrorResponseSchema,
7695
+ 404: ErrorResponseSchema,
7696
+ 500: ErrorResponseSchema
7697
+ }
7698
+ },
7699
+ validateAgent: {
7700
+ method: "POST",
7701
+ path: apiPaths.validateAgent,
7702
+ body: UpsertCustomAgentRequestSchema,
7703
+ responses: {
7704
+ 200: ValidateCustomAgentResponseSchema,
7705
+ 400: ErrorResponseSchema,
7706
+ 500: ErrorResponseSchema
7707
+ }
7708
+ },
7606
7709
  attachAgentsWorktreeConversation: {
7607
7710
  method: "POST",
7608
7711
  path: apiPaths.attachAgentsWorktreeConversation,
@@ -15342,6 +15445,38 @@ function parseProfiles(raw, includeDefaultProfile) {
15342
15445
  }
15343
15446
  return profiles;
15344
15447
  }
15448
+ function cloneAgentConfig(agent) {
15449
+ return { ...agent };
15450
+ }
15451
+ function cloneAgents(agents) {
15452
+ return Object.fromEntries(Object.entries(agents).map(([id, agent]) => [id, cloneAgentConfig(agent)]));
15453
+ }
15454
+ function parseCustomAgent(raw) {
15455
+ if (!isRecord4(raw))
15456
+ return null;
15457
+ if (typeof raw.label !== "string" || !raw.label.trim())
15458
+ return null;
15459
+ if (typeof raw.startCommand !== "string" || !raw.startCommand.trim())
15460
+ return null;
15461
+ return {
15462
+ label: raw.label.trim(),
15463
+ startCommand: raw.startCommand.trim(),
15464
+ ...typeof raw.resumeCommand === "string" && raw.resumeCommand.trim() ? { resumeCommand: raw.resumeCommand.trim() } : {}
15465
+ };
15466
+ }
15467
+ function parseCustomAgents(raw) {
15468
+ if (!isRecord4(raw))
15469
+ return {};
15470
+ return Object.entries(raw).reduce((acc, [id, value]) => {
15471
+ if (!id.trim())
15472
+ return acc;
15473
+ const parsed = parseCustomAgent(value);
15474
+ if (parsed) {
15475
+ acc[id.trim()] = parsed;
15476
+ }
15477
+ return acc;
15478
+ }, {});
15479
+ }
15345
15480
  function parseServices(raw) {
15346
15481
  if (!Array.isArray(raw))
15347
15482
  return [];
@@ -15434,6 +15569,7 @@ function parseProjectConfig(parsed) {
15434
15569
  autoPull: isRecord4(parsed.workspace) ? parseAutoPull(parsed.workspace.autoPull) : DEFAULT_CONFIG.workspace.autoPull
15435
15570
  },
15436
15571
  profiles: parseProfiles(parsed.profiles, true),
15572
+ agents: {},
15437
15573
  services: parseServices(parsed.services),
15438
15574
  startupEnvs: parseStartupEnvs(parsed.startupEnvs),
15439
15575
  integrations: {
@@ -15501,20 +15637,21 @@ function loadLocalProjectConfigOverlay(root) {
15501
15637
  try {
15502
15638
  const text = readLocalConfigFile(root).trim();
15503
15639
  if (!text) {
15504
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
15640
+ return { worktreeRoot: null, profiles: {}, agents: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
15505
15641
  }
15506
15642
  const parsed = parseConfigDocument(text);
15507
15643
  const ws = isRecord4(parsed.workspace) ? parsed.workspace : null;
15508
15644
  return {
15509
15645
  worktreeRoot: ws && typeof ws.worktreeRoot === "string" ? ws.worktreeRoot : null,
15510
15646
  profiles: parseProfiles(parsed.profiles, false),
15647
+ agents: parseCustomAgents(parsed.agents),
15511
15648
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
15512
15649
  linear: parseLocalLinearOverlay(parsed),
15513
15650
  github: parseLocalGitHubOverlay(parsed),
15514
15651
  autoPull: parseLocalAutoPullOverlay(parsed)
15515
15652
  };
15516
15653
  } catch {
15517
- return { worktreeRoot: null, profiles: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
15654
+ return { worktreeRoot: null, profiles: {}, agents: {}, lifecycleHooks: {}, linear: null, github: null, autoPull: null };
15518
15655
  }
15519
15656
  }
15520
15657
  function mergeHookCommand(projectCommand, localCommand) {
@@ -15574,6 +15711,10 @@ function loadConfig(dir, options = {}) {
15574
15711
  ...cloneProfiles(projectConfig.profiles),
15575
15712
  ...cloneProfiles(localOverlay.profiles)
15576
15713
  },
15714
+ agents: {
15715
+ ...cloneAgents(projectConfig.agents),
15716
+ ...cloneAgents(localOverlay.agents)
15717
+ },
15577
15718
  lifecycleHooks: mergeLifecycleHooks(projectConfig.lifecycleHooks, localOverlay.lifecycleHooks),
15578
15719
  integrations
15579
15720
  };
@@ -15603,6 +15744,7 @@ var init_config = __esm(() => {
15603
15744
  panes: clonePanes(DEFAULT_PANES)
15604
15745
  }
15605
15746
  },
15747
+ agents: {},
15606
15748
  services: [],
15607
15749
  startupEnvs: {},
15608
15750
  integrations: {
@@ -16184,7 +16326,7 @@ class AutoNameService {
16184
16326
  return normalizeGeneratedBranchName(output);
16185
16327
  }
16186
16328
  }
16187
- var MAX_BRANCH_LENGTH = 40, DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001", AUTO_NAME_TIMEOUT_MS = 1e4, DEFAULT_SYSTEM_PROMPT, AutoNameTimeoutError;
16329
+ var MAX_BRANCH_LENGTH = 40, DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001", AUTO_NAME_TIMEOUT_MS = 15000, DEFAULT_SYSTEM_PROMPT, AutoNameTimeoutError;
16188
16330
  var init_auto_name_service = __esm(() => {
16189
16331
  init_policies();
16190
16332
  init_branch_name();
@@ -16271,7 +16413,7 @@ var init_archive_state_service = __esm(() => {
16271
16413
 
16272
16414
  // backend/src/adapters/agent-runtime.ts
16273
16415
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
16274
- import { dirname as dirname4, join as join9 } from "path";
16416
+ import { dirname as dirname4, join as join9, resolve as resolve6 } from "path";
16275
16417
  function shellQuote(value) {
16276
16418
  return `'${value.replaceAll("'", "'\\''")}'`;
16277
16419
  }
@@ -16290,6 +16432,7 @@ from pathlib import Path
16290
16432
 
16291
16433
 
16292
16434
  CONTROL_ENV_PATH = Path(__file__).resolve().with_name("control.env")
16435
+ CONTROL_REQUEST_TIMEOUT_SECONDS = 2
16293
16436
 
16294
16437
 
16295
16438
  def read_control_env():
@@ -16319,6 +16462,7 @@ def build_parser():
16319
16462
 
16320
16463
  status_changed = subparsers.add_parser("status-changed")
16321
16464
  status_changed.add_argument("--lifecycle", choices=["starting", "running", "idle", "stopped"], required=True)
16465
+ status_changed.add_argument("--best-effort", action="store_true")
16322
16466
 
16323
16467
  pr_opened = subparsers.add_parser("pr-opened")
16324
16468
  pr_opened.add_argument("--url")
@@ -16328,6 +16472,11 @@ def build_parser():
16328
16472
 
16329
16473
  subparsers.add_parser("claude-user-prompt-submit")
16330
16474
  subparsers.add_parser("claude-post-tool-use")
16475
+ subparsers.add_parser("codex-session-start")
16476
+ subparsers.add_parser("codex-user-prompt-submit")
16477
+ subparsers.add_parser("codex-permission-request")
16478
+ subparsers.add_parser("codex-post-tool-use")
16479
+ subparsers.add_parser("codex-stop")
16331
16480
 
16332
16481
  return parser
16333
16482
 
@@ -16370,6 +16519,41 @@ def read_hook_payload():
16370
16519
  return parsed if isinstance(parsed, dict) else {}
16371
16520
 
16372
16521
 
16522
+ def iter_string_values(value):
16523
+ if isinstance(value, str):
16524
+ yield value
16525
+ return
16526
+ if isinstance(value, dict):
16527
+ for child in value.values():
16528
+ yield from iter_string_values(child)
16529
+ return
16530
+ if isinstance(value, list):
16531
+ for child in value:
16532
+ yield from iter_string_values(child)
16533
+
16534
+
16535
+ def find_pr_url(value):
16536
+ for text in iter_string_values(value):
16537
+ match = re.search(r"https://github\\.com/[^\\s\\"]+/pull/\\d+", text)
16538
+ if match:
16539
+ return match.group(0)
16540
+ return None
16541
+
16542
+
16543
+ def maybe_send_pr_opened(hook_payload, control_env):
16544
+ tool_name = hook_payload.get("tool_name")
16545
+ tool_input = hook_payload.get("tool_input")
16546
+ if not isinstance(tool_input, dict) or tool_name != "Bash":
16547
+ return True
16548
+
16549
+ command = tool_input.get("command")
16550
+ if not isinstance(command, str) or "gh pr create" not in command:
16551
+ return True
16552
+
16553
+ pr_args = argparse.Namespace(url=find_pr_url(hook_payload.get("tool_response")))
16554
+ return send_payload(build_payload("pr-opened", pr_args, control_env), control_env)
16555
+
16556
+
16373
16557
  def send_payload(payload, control_env):
16374
16558
  request = urllib.request.Request(
16375
16559
  control_env["WEBMUX_CONTROL_URL"],
@@ -16382,7 +16566,7 @@ def send_payload(payload, control_env):
16382
16566
  )
16383
16567
 
16384
16568
  try:
16385
- with urllib.request.urlopen(request, timeout=10) as response:
16569
+ with urllib.request.urlopen(request, timeout=CONTROL_REQUEST_TIMEOUT_SECONDS) as response:
16386
16570
  if response.status < 200 or response.status >= 300:
16387
16571
  print(f"control endpoint returned HTTP {response.status}", file=sys.stderr)
16388
16572
  return False
@@ -16416,34 +16600,40 @@ def main():
16416
16600
  print(f"missing control env keys: {', '.join(missing)}", file=sys.stderr)
16417
16601
  return 1
16418
16602
 
16603
+ if parsed.command == "codex-session-start":
16604
+ send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="idle"), control_env), control_env)
16605
+ return 0
16606
+
16607
+ if parsed.command == "codex-user-prompt-submit":
16608
+ send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="running"), control_env), control_env)
16609
+ return 0
16610
+
16419
16611
  if parsed.command == "claude-user-prompt-submit":
16420
16612
  if not send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="running"), control_env), control_env):
16421
16613
  return 1
16422
16614
  return 0
16423
16615
 
16424
- if parsed.command == "claude-post-tool-use":
16425
- hook_payload = read_hook_payload()
16426
- tool_name = hook_payload.get("tool_name")
16427
- tool_input = hook_payload.get("tool_input")
16428
- if not isinstance(tool_input, dict) or tool_name != "Bash":
16429
- return 0
16616
+ if parsed.command == "codex-permission-request":
16617
+ send_payload(build_payload("status-changed", argparse.Namespace(lifecycle="idle"), control_env), control_env)
16618
+ return 0
16430
16619
 
16431
- command = tool_input.get("command")
16432
- if not isinstance(command, str) or "gh pr create" not in command:
16433
- return 0
16620
+ if parsed.command == "codex-post-tool-use":
16621
+ hook_payload = read_hook_payload()
16622
+ maybe_send_pr_opened(hook_payload, control_env)
16623
+ return 0
16434
16624
 
16435
- pr_args = argparse.Namespace(url=None)
16436
- tool_response = hook_payload.get("tool_response")
16437
- if isinstance(tool_response, str):
16438
- match = re.search(r"https://github\\.com/[^\\s\\"]+/pull/\\d+", tool_response)
16439
- if match:
16440
- pr_args.url = match.group(0)
16625
+ if parsed.command == "claude-post-tool-use":
16626
+ hook_payload = read_hook_payload()
16627
+ return 0 if maybe_send_pr_opened(hook_payload, control_env) else 1
16441
16628
 
16442
- return 0 if send_payload(build_payload("pr-opened", pr_args, control_env), control_env) else 1
16629
+ if parsed.command == "codex-stop":
16630
+ send_payload(build_payload("agent-stopped", parsed, control_env), control_env)
16631
+ print(json.dumps({}))
16632
+ return 0
16443
16633
 
16444
16634
  payload = build_payload(parsed.command, parsed, control_env)
16445
16635
  if not send_payload(payload, control_env):
16446
- return 1
16636
+ return 0 if getattr(parsed, "best_effort", False) else 1
16447
16637
 
16448
16638
  return 0
16449
16639
 
@@ -16513,6 +16703,81 @@ function buildClaudeHookSettings(input) {
16513
16703
  }
16514
16704
  };
16515
16705
  }
16706
+ function buildCodexHookSettings(input) {
16707
+ const statusCommand = `${shellQuote(input.agentCtlPath)} status-changed --lifecycle running --best-effort`;
16708
+ return {
16709
+ hooks: {
16710
+ SessionStart: [
16711
+ {
16712
+ matcher: "startup|resume|clear",
16713
+ hooks: [
16714
+ {
16715
+ type: "command",
16716
+ command: `${shellQuote(input.agentCtlPath)} codex-session-start`,
16717
+ timeout: 30
16718
+ }
16719
+ ]
16720
+ }
16721
+ ],
16722
+ UserPromptSubmit: [
16723
+ {
16724
+ hooks: [
16725
+ {
16726
+ type: "command",
16727
+ command: `${shellQuote(input.agentCtlPath)} codex-user-prompt-submit`,
16728
+ timeout: 30
16729
+ }
16730
+ ]
16731
+ }
16732
+ ],
16733
+ PermissionRequest: [
16734
+ {
16735
+ hooks: [
16736
+ {
16737
+ type: "command",
16738
+ command: `${shellQuote(input.agentCtlPath)} codex-permission-request`,
16739
+ timeout: 30
16740
+ }
16741
+ ]
16742
+ }
16743
+ ],
16744
+ PreToolUse: [
16745
+ {
16746
+ hooks: [
16747
+ {
16748
+ type: "command",
16749
+ command: statusCommand,
16750
+ timeout: 30
16751
+ }
16752
+ ]
16753
+ }
16754
+ ],
16755
+ PostToolUse: [
16756
+ {
16757
+ matcher: "Bash",
16758
+ hooks: [
16759
+ {
16760
+ type: "command",
16761
+ command: `${shellQuote(input.agentCtlPath)} codex-post-tool-use`,
16762
+ timeout: 30
16763
+ }
16764
+ ]
16765
+ }
16766
+ ],
16767
+ Stop: [
16768
+ {
16769
+ hooks: [
16770
+ {
16771
+ type: "command",
16772
+ command: `${shellQuote(input.agentCtlPath)} codex-stop`,
16773
+ timeout: 30
16774
+ }
16775
+ ]
16776
+ }
16777
+ ]
16778
+ }
16779
+ };
16780
+ }
16516
16781
  async function mergeClaudeSettings(settingsPath, hookSettings) {
16517
16782
  let existing = {};
16518
16783
  try {
@@ -16532,13 +16797,77 @@ async function mergeClaudeSettings(settingsPath, hookSettings) {
16532
16797
  await Bun.write(settingsPath, JSON.stringify(merged, null, 2) + `
16533
16798
  `);
16534
16799
  }
16800
+ function commandStartsWithAgentCtl(command, agentCtlPath) {
16801
+ const trimmedCommand = command.trimStart();
16802
+ const quotedAgentCtlPath = shellQuote(agentCtlPath);
16803
+ return trimmedCommand === agentCtlPath || trimmedCommand.startsWith(`${agentCtlPath} `) || trimmedCommand === quotedAgentCtlPath || trimmedCommand.startsWith(`${quotedAgentCtlPath} `);
16804
+ }
16805
+ function isWebmuxHookGroup(group, agentCtlPath) {
16806
+ if (!isRecord5(group) || !Array.isArray(group.hooks))
16807
+ return false;
16808
+ return group.hooks.some((hook) => isRecord5(hook) && typeof hook.command === "string" && commandStartsWithAgentCtl(hook.command, agentCtlPath));
16809
+ }
16810
+ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
16811
+ let existing = {};
16812
+ try {
16813
+ const file = Bun.file(hooksPath);
16814
+ if (await file.exists()) {
16815
+ const parsed = await file.json();
16816
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
16817
+ existing = parsed;
16818
+ }
16819
+ }
16820
+ } catch {
16821
+ existing = {};
16822
+ }
16823
+ const existingHooks = isRecord5(existing.hooks) ? existing.hooks : {};
16824
+ const mergedHooks = { ...existingHooks };
16825
+ for (const [eventName, groups] of Object.entries(hookSettings)) {
16826
+ const eventGroups = existingHooks[eventName];
16827
+ const preservedGroups = Array.isArray(eventGroups) ? eventGroups.filter((group) => !isWebmuxHookGroup(group, agentCtlPath)) : [];
16828
+ mergedHooks[eventName] = [...preservedGroups, ...groups];
16829
+ }
16830
+ await Bun.write(hooksPath, JSON.stringify({ ...existing, hooks: mergedHooks }, null, 2) + `
16831
+ `);
16832
+ }
16833
+ async function resolveGitCommonDir(gitDir) {
16834
+ try {
16835
+ const commonDir = (await Bun.file(join9(gitDir, "commondir")).text()).trim();
16836
+ if (!commonDir)
16837
+ return gitDir;
16838
+ return commonDir.startsWith("/") ? commonDir : resolve6(gitDir, commonDir);
16839
+ } catch {
16840
+ return gitDir;
16841
+ }
16842
+ }
16843
+ async function ensureGeneratedCodexHooksIgnored(gitDir) {
16844
+ const commonDir = await resolveGitCommonDir(gitDir);
16845
+ const excludePath = join9(commonDir, "info", "exclude");
16846
+ let existing = "";
16847
+ try {
16848
+ existing = await Bun.file(excludePath).text();
16849
+ } catch {
16850
+ existing = "";
16851
+ }
16852
+ const lines = existing.split(/\r?\n/).map((line) => line.trim());
16853
+ if (lines.includes(GENERATED_CODEX_HOOKS_EXCLUDE))
16854
+ return;
16855
+ await mkdir3(dirname4(excludePath), { recursive: true });
16856
+ const separator = existing.length > 0 && !existing.endsWith(`
16857
+ `) ? `
16858
+ ` : "";
16859
+ await Bun.write(excludePath, `${existing}${separator}${GENERATED_CODEX_HOOKS_EXCLUDE}
16860
+ `);
16861
+ }
16535
16862
  async function ensureAgentRuntimeArtifacts(input) {
16536
16863
  const storagePaths = getWorktreeStoragePaths(input.gitDir);
16537
16864
  const artifacts = {
16538
16865
  agentCtlPath: join9(storagePaths.webmuxDir, "webmux-agentctl"),
16539
- claudeSettingsPath: join9(input.worktreePath, ".claude", "settings.local.json")
16866
+ claudeSettingsPath: join9(input.worktreePath, ".claude", "settings.local.json"),
16867
+ codexHooksPath: join9(input.worktreePath, ".codex", "hooks.json")
16540
16868
  };
16541
16869
  await mkdir3(dirname4(artifacts.claudeSettingsPath), { recursive: true });
16870
+ await mkdir3(dirname4(artifacts.codexHooksPath), { recursive: true });
16542
16871
  await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
16543
16872
  await chmod2(artifacts.agentCtlPath, 493);
16544
16873
  const hookSettings = buildClaudeHookSettings(artifacts);
@@ -16547,8 +16876,11 @@ async function ensureAgentRuntimeArtifacts(input) {
16547
16876
  throw new Error("Invalid Claude hook settings");
16548
16877
  }
16549
16878
  await mergeClaudeSettings(artifacts.claudeSettingsPath, hooks);
16879
+ await ensureGeneratedCodexHooksIgnored(input.gitDir);
16880
+ await mergeCodexHooksFile(artifacts.codexHooksPath, buildCodexHookSettings(artifacts).hooks, artifacts.agentCtlPath);
16550
16881
  return artifacts;
16551
16882
  }
16883
+ var GENERATED_CODEX_HOOKS_EXCLUDE = ".codex/hooks.json";
16552
16884
  var init_agent_runtime = __esm(() => {
16553
16885
  init_fs();
16554
16886
  });
@@ -16563,17 +16895,18 @@ function buildRuntimeBootstrap(runtimeEnvPath) {
16563
16895
  function buildDockerRuntimeBootstrap(runtimeEnvPath) {
16564
16896
  return `${buildRuntimeBootstrap(runtimeEnvPath)}; export PATH="$PATH:${DOCKER_PATH_FALLBACK}"`;
16565
16897
  }
16566
- function buildAgentInvocation(input) {
16898
+ function buildBuiltInAgentInvocation(input) {
16567
16899
  if (input.agent === "codex") {
16900
+ const hooksFlag = " --enable codex_hooks";
16568
16901
  const yoloFlag2 = input.yolo ? " --yolo" : "";
16569
16902
  if (input.launchMode === "resume") {
16570
- return `codex${yoloFlag2} resume --last`;
16903
+ return `codex${hooksFlag}${yoloFlag2} resume --last`;
16571
16904
  }
16572
16905
  const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
16573
16906
  if (input.systemPrompt) {
16574
- return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
16907
+ return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
16575
16908
  }
16576
- return `codex${yoloFlag2}${promptSuffix2}`;
16909
+ return `codex${hooksFlag}${yoloFlag2}${promptSuffix2}`;
16577
16910
  }
16578
16911
  const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
16579
16912
  if (input.launchMode === "resume") {
@@ -16585,6 +16918,47 @@ function buildAgentInvocation(input) {
16585
16918
  }
16586
16919
  return `claude${yoloFlag}${promptSuffix}`;
16587
16920
  }
16921
+ function renderCustomCommandTemplate(template) {
16922
+ return template.replaceAll("${PROMPT}", `$${CUSTOM_AGENT_TEMPLATE_VARS.PROMPT}`).replaceAll("${SYSTEM_PROMPT}", `$${CUSTOM_AGENT_TEMPLATE_VARS.SYSTEM_PROMPT}`).replaceAll("${WORKTREE_PATH}", `$${CUSTOM_AGENT_TEMPLATE_VARS.WORKTREE_PATH}`).replaceAll("${REPO_PATH}", `$${CUSTOM_AGENT_TEMPLATE_VARS.REPO_PATH}`).replaceAll("${BRANCH}", `$${CUSTOM_AGENT_TEMPLATE_VARS.BRANCH}`).replaceAll("${PROFILE}", `$${CUSTOM_AGENT_TEMPLATE_VARS.PROFILE}`);
16923
+ }
16924
+ function buildCustomAgentExports(input) {
16925
+ const envEntries = [
16926
+ [CUSTOM_AGENT_TEMPLATE_VARS.PROMPT, input.prompt ?? ""],
16927
+ [CUSTOM_AGENT_TEMPLATE_VARS.SYSTEM_PROMPT, input.systemPrompt ?? ""],
16928
+ [CUSTOM_AGENT_TEMPLATE_VARS.WORKTREE_PATH, input.worktreePath],
16929
+ [CUSTOM_AGENT_TEMPLATE_VARS.REPO_PATH, input.repoRoot],
16930
+ [CUSTOM_AGENT_TEMPLATE_VARS.BRANCH, input.branch],
16931
+ [CUSTOM_AGENT_TEMPLATE_VARS.PROFILE, input.profileName]
16932
+ ];
16933
+ return envEntries.map(([key, value]) => `export ${key}=${quoteShell(value)}`).join("; ");
16934
+ }
16935
+ function buildCustomAgentInvocation(input) {
16936
+ const template = input.launchMode === "resume" && input.agent.implementation.config.resumeCommand ? input.agent.implementation.config.resumeCommand : input.agent.implementation.config.startCommand;
16937
+ const exports = buildCustomAgentExports(input);
16938
+ const renderedCommand = renderCustomCommandTemplate(template);
16939
+ return `${exports}; ${renderedCommand}`;
16940
+ }
16941
+ function buildAgentInvocation(input) {
16942
+ if (input.agent.kind === "builtin") {
16943
+ return buildBuiltInAgentInvocation({
16944
+ agent: input.agent.implementation.agent,
16945
+ yolo: input.yolo,
16946
+ systemPrompt: input.systemPrompt,
16947
+ prompt: input.prompt,
16948
+ launchMode: input.launchMode
16949
+ });
16950
+ }
16951
+ return buildCustomAgentInvocation({
16952
+ agent: input.agent,
16953
+ systemPrompt: input.systemPrompt,
16954
+ prompt: input.prompt,
16955
+ worktreePath: input.worktreePath,
16956
+ repoRoot: input.repoRoot,
16957
+ branch: input.branch,
16958
+ profileName: input.profileName,
16959
+ launchMode: input.launchMode
16960
+ });
16961
+ }
16588
16962
  function buildAgentCommand(input, bootstrap = buildRuntimeBootstrap) {
16589
16963
  return `${bootstrap(input.runtimeEnvPath)}; ${buildAgentInvocation(input)}`;
16590
16964
  }
@@ -16603,10 +16977,112 @@ function buildDockerShellCommand(containerName2, worktreePath, runtimeEnvPath, s
16603
16977
  function buildDockerAgentPaneCommand(input) {
16604
16978
  return buildAgentCommand(input, buildDockerRuntimeBootstrap);
16605
16979
  }
16606
- var DOCKER_PATH_FALLBACK = "/root/.local/bin:/usr/local/bin:/root/.bun/bin:/root/.cargo/bin";
16980
+ var DOCKER_PATH_FALLBACK = "/root/.local/bin:/usr/local/bin:/root/.bun/bin:/root/.cargo/bin", CUSTOM_AGENT_TEMPLATE_VARS;
16981
+ var init_agent_service = __esm(() => {
16982
+ CUSTOM_AGENT_TEMPLATE_VARS = {
16983
+ PROMPT: "WEBMUX_AGENT_PROMPT",
16984
+ SYSTEM_PROMPT: "WEBMUX_AGENT_SYSTEM_PROMPT",
16985
+ WORKTREE_PATH: "WEBMUX_AGENT_WORKTREE_PATH",
16986
+ REPO_PATH: "WEBMUX_AGENT_REPO_PATH",
16987
+ BRANCH: "WEBMUX_AGENT_BRANCH",
16988
+ PROFILE: "WEBMUX_AGENT_PROFILE"
16989
+ };
16990
+ });
16991
+
16992
+ // backend/src/services/agent-registry.ts
16993
+ function cloneCapabilities(capabilities) {
16994
+ return { ...capabilities };
16995
+ }
16996
+ function cloneDefinition(definition) {
16997
+ if (definition.kind === "builtin") {
16998
+ return {
16999
+ ...definition,
17000
+ capabilities: cloneCapabilities(definition.capabilities),
17001
+ implementation: { ...definition.implementation }
17002
+ };
17003
+ }
17004
+ return {
17005
+ ...definition,
17006
+ capabilities: cloneCapabilities(definition.capabilities),
17007
+ implementation: {
17008
+ type: "custom",
17009
+ config: { ...definition.implementation.config }
17010
+ }
17011
+ };
17012
+ }
17013
+ function buildCustomAgentDefinition(id, config) {
17014
+ return {
17015
+ id,
17016
+ label: config.label,
17017
+ kind: "custom",
17018
+ capabilities: {
17019
+ terminal: true,
17020
+ inAppChat: false,
17021
+ conversationHistory: false,
17022
+ interrupt: false,
17023
+ resume: config.resumeCommand !== undefined
17024
+ },
17025
+ implementation: {
17026
+ type: "custom",
17027
+ config: { ...config }
17028
+ }
17029
+ };
17030
+ }
17031
+ function listAgentDefinitions(config) {
17032
+ const builtInIds = new Set(BUILTIN_AGENT_DEFINITIONS.map((agent) => agent.id));
17033
+ const customDefinitions = Object.entries(config.agents).filter(([id]) => !builtInIds.has(id)).sort(([leftId, left], [rightId, right]) => {
17034
+ const labelCompare = left.label.localeCompare(right.label);
17035
+ return labelCompare !== 0 ? labelCompare : leftId.localeCompare(rightId);
17036
+ }).map(([id, agent]) => buildCustomAgentDefinition(id, agent));
17037
+ return [
17038
+ ...BUILTIN_AGENT_DEFINITIONS.map((definition) => cloneDefinition(definition)),
17039
+ ...customDefinitions
17040
+ ];
17041
+ }
17042
+ function getAgentDefinition(config, agentId) {
17043
+ const definition = listAgentDefinitions(config).find((agent) => agent.id === agentId);
17044
+ return definition ?? null;
17045
+ }
17046
+ var BUILTIN_AGENT_DEFINITIONS;
17047
+ var init_agent_registry = __esm(() => {
17048
+ BUILTIN_AGENT_DEFINITIONS = [
17049
+ {
17050
+ id: "claude",
17051
+ label: "Claude",
17052
+ kind: "builtin",
17053
+ capabilities: {
17054
+ terminal: true,
17055
+ inAppChat: true,
17056
+ conversationHistory: true,
17057
+ interrupt: true,
17058
+ resume: true
17059
+ },
17060
+ implementation: {
17061
+ type: "builtin",
17062
+ agent: "claude"
17063
+ }
17064
+ },
17065
+ {
17066
+ id: "codex",
17067
+ label: "Codex",
17068
+ kind: "builtin",
17069
+ capabilities: {
17070
+ terminal: true,
17071
+ inAppChat: true,
17072
+ conversationHistory: true,
17073
+ interrupt: true,
17074
+ resume: true
17075
+ },
17076
+ implementation: {
17077
+ type: "builtin",
17078
+ agent: "codex"
17079
+ }
17080
+ }
17081
+ ];
17082
+ });
16607
17083
 
16608
17084
  // backend/src/services/session-service.ts
16609
- import { resolve as resolve6 } from "path";
17085
+ import { resolve as resolve7 } from "path";
16610
17086
  function quoteShell2(value) {
16611
17087
  return `'${value.replaceAll("'", "'\\''")}'`;
16612
17088
  }
@@ -16620,7 +17096,7 @@ function buildCommandPaneStartupCommand(template, ctx) {
16620
17096
  if (!template.workingDir) {
16621
17097
  return template.command;
16622
17098
  }
16623
- const workingDir = resolve6(resolvePaneCwd(template, ctx), template.workingDir);
17099
+ const workingDir = resolve7(resolvePaneCwd(template, ctx), template.workingDir);
16624
17100
  return `cd -- ${quoteShell2(workingDir)} && ${template.command}`;
16625
17101
  }
16626
17102
  function resolvePaneStartupCommand(template, ctx) {
@@ -16850,7 +17326,7 @@ var init_worktree_service = __esm(() => {
16850
17326
 
16851
17327
  // backend/src/services/lifecycle-service.ts
16852
17328
  import { mkdir as mkdir4 } from "fs/promises";
16853
- import { dirname as dirname5, resolve as resolve7 } from "path";
17329
+ import { dirname as dirname5, resolve as resolve8 } from "path";
16854
17330
  function toErrorMessage2(error) {
16855
17331
  return error instanceof Error ? error.message : String(error);
16856
17332
  }
@@ -16880,14 +17356,15 @@ function buildRuntimeControlBaseUrl(controlBaseUrl, runtime) {
16880
17356
  function prefixAgentBranch(agent, branch) {
16881
17357
  return `${agent}-${branch}`;
16882
17358
  }
16883
- function buildCreateWorktreeTargets(branch, agentSelection) {
16884
- if (agentSelection === "both") {
16885
- return [
16886
- { branch: prefixAgentBranch("claude", branch), agent: "claude" },
16887
- { branch: prefixAgentBranch("codex", branch), agent: "codex" }
16888
- ];
17359
+ function buildCreateWorktreeTargets(branch, agentIds) {
17360
+ if (agentIds.length <= 1) {
17361
+ const agent = agentIds[0];
17362
+ return agent ? [{ branch, agent }] : [];
16889
17363
  }
16890
- return [{ branch, agent: agentSelection }];
17364
+ return agentIds.map((agent) => ({
17365
+ branch: prefixAgentBranch(agent, branch),
17366
+ agent
17367
+ }));
16891
17368
  }
16892
17369
 
16893
17370
  class LifecycleService {
@@ -16897,12 +17374,12 @@ class LifecycleService {
16897
17374
  }
16898
17375
  async createWorktrees(input) {
16899
17376
  const mode = input.mode ?? "new";
16900
- const agentSelection = input.agent ?? this.deps.config.workspace.defaultAgent;
16901
- if (agentSelection === "both" && mode === "existing") {
16902
- throw new LifecycleError("Creating both agents is only supported for new worktrees", 400);
17377
+ const agentIds = this.resolveSelectedAgents(input);
17378
+ if (agentIds.length > 1 && mode === "existing") {
17379
+ throw new LifecycleError("Creating multiple agents is only supported for new worktrees", 400);
16903
17380
  }
16904
17381
  const branch = await this.resolveBranch(input.branch, input.prompt, mode);
16905
- const targets = buildCreateWorktreeTargets(branch, agentSelection);
17382
+ const targets = buildCreateWorktreeTargets(branch, agentIds);
16906
17383
  const createdBranches = [];
16907
17384
  try {
16908
17385
  for (const target of targets) {
@@ -16929,28 +17406,30 @@ class LifecycleService {
16929
17406
  async createWorktree(input) {
16930
17407
  const mode = input.mode ?? "new";
16931
17408
  const branch = await this.resolveBranch(input.branch, input.prompt, mode);
16932
- const agent = this.resolveAgent(input.agent);
17409
+ const agent = this.resolveAgentDefinition(input.agent);
16933
17410
  return await this.createResolvedWorktree({
16934
17411
  ...input,
16935
17412
  mode,
16936
17413
  branch,
16937
- agent
17414
+ agent: agent.id
16938
17415
  });
16939
17416
  }
16940
17417
  async openWorktree(branch) {
16941
17418
  try {
16942
17419
  const resolved = await this.resolveExistingWorktree(branch);
16943
- const launchMode = resolved.meta ? "resume" : "fresh";
16944
17420
  const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
16945
- const { profile } = this.resolveProfile(initialized.meta.profile);
17421
+ const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
17422
+ const agent = this.resolveAgentDefinition(initialized.meta.agent);
17423
+ const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
16946
17424
  await ensureAgentRuntimeArtifacts({
16947
17425
  gitDir: initialized.paths.gitDir,
16948
17426
  worktreePath: resolved.entry.path
16949
17427
  });
16950
17428
  await this.materializeRuntimeSession({
16951
17429
  branch,
17430
+ profileName,
16952
17431
  profile,
16953
- agent: initialized.meta.agent,
17432
+ agent,
16954
17433
  initialized,
16955
17434
  worktreePath: resolved.entry.path,
16956
17435
  launchMode
@@ -17084,14 +17563,22 @@ class LifecycleService {
17084
17563
  profile
17085
17564
  };
17086
17565
  }
17087
- resolveAgent(agent) {
17088
- if (!agent)
17089
- return this.deps.config.workspace.defaultAgent;
17090
- if (agent !== "claude" && agent !== "codex") {
17091
- throw new LifecycleError(`Unknown agent: ${agent}`, 400);
17566
+ resolveAgentDefinition(agentId) {
17567
+ const resolvedAgentId = agentId ?? this.deps.config.workspace.defaultAgent;
17568
+ const agent = getAgentDefinition(this.deps.config, resolvedAgentId);
17569
+ if (!agent) {
17570
+ throw new LifecycleError(`Unknown agent: ${resolvedAgentId}`, 400);
17092
17571
  }
17093
17572
  return agent;
17094
17573
  }
17574
+ resolveSelectedAgents(input) {
17575
+ const selectedAgents = input.agents && input.agents.length > 0 ? input.agents : [input.agent ?? this.deps.config.workspace.defaultAgent];
17576
+ const dedupedAgentIds = [...new Set(selectedAgents.map((agent) => agent.trim()).filter((agent) => agent.length > 0))];
17577
+ if (dedupedAgentIds.length === 0) {
17578
+ throw new LifecycleError("At least one agent must be selected", 400);
17579
+ }
17580
+ return dedupedAgentIds.map((agentId) => this.resolveAgentDefinition(agentId).id);
17581
+ }
17095
17582
  async buildStartupEnvValues(envOverrides) {
17096
17583
  const startupEnvValues = Object.fromEntries(Object.entries(this.deps.config.startupEnvs).map(([key, value]) => [key, stringifyStartupEnvValue(value)]));
17097
17584
  for (const [key, value] of Object.entries(envOverrides ?? {})) {
@@ -17107,20 +17594,20 @@ class LifecycleService {
17107
17594
  return allocateServicePorts(metas, this.deps.config.services);
17108
17595
  }
17109
17596
  resolveWorktreePath(branch) {
17110
- return resolve7(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
17597
+ return resolve8(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
17111
17598
  }
17112
17599
  listLocalBranches() {
17113
- return this.deps.git.listLocalBranches(resolve7(this.deps.projectRoot));
17600
+ return this.deps.git.listLocalBranches(resolve8(this.deps.projectRoot));
17114
17601
  }
17115
17602
  listRemoteBranches() {
17116
- return this.deps.git.listRemoteBranches(resolve7(this.deps.projectRoot));
17603
+ return this.deps.git.listRemoteBranches(resolve8(this.deps.projectRoot));
17117
17604
  }
17118
17605
  listCheckedOutBranches() {
17119
- return new Set(this.deps.git.listWorktrees(resolve7(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
17606
+ return new Set(this.deps.git.listWorktrees(resolve8(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
17120
17607
  }
17121
17608
  listProjectWorktrees() {
17122
- const projectRoot2 = resolve7(this.deps.projectRoot);
17123
- return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve7(entry.path) !== projectRoot2);
17609
+ const projectRoot2 = resolve8(this.deps.projectRoot);
17610
+ return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve8(entry.path) !== projectRoot2);
17124
17611
  }
17125
17612
  async readManagedMetas() {
17126
17613
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -17213,6 +17700,7 @@ class LifecycleService {
17213
17700
  });
17214
17701
  ensureSessionLayout(this.deps.tmux, this.buildSessionLayout({
17215
17702
  branch: input.branch,
17703
+ profileName: input.profileName,
17216
17704
  profile: input.profile,
17217
17705
  agent: input.agent,
17218
17706
  initialized: input.initialized,
@@ -17225,6 +17713,7 @@ class LifecycleService {
17225
17713
  }
17226
17714
  ensureSessionLayout(this.deps.tmux, this.buildSessionLayout({
17227
17715
  branch: input.branch,
17716
+ profileName: input.profileName,
17228
17717
  profile: input.profile,
17229
17718
  agent: input.agent,
17230
17719
  initialized: input.initialized,
@@ -17243,6 +17732,10 @@ class LifecycleService {
17243
17732
  agent: buildDockerAgentPaneCommand({
17244
17733
  agent: input.agent,
17245
17734
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
17735
+ repoRoot: this.deps.projectRoot,
17736
+ worktreePath: input.worktreePath,
17737
+ branch: input.branch,
17738
+ profileName: input.profileName,
17246
17739
  yolo: input.profile.yolo === true,
17247
17740
  systemPrompt,
17248
17741
  prompt: input.launchMode === "fresh" ? input.prompt : undefined,
@@ -17253,6 +17746,10 @@ class LifecycleService {
17253
17746
  agent: buildAgentPaneCommand({
17254
17747
  agent: input.agent,
17255
17748
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
17749
+ repoRoot: this.deps.projectRoot,
17750
+ worktreePath: input.worktreePath,
17751
+ branch: input.branch,
17752
+ profileName: input.profileName,
17256
17753
  yolo: input.profile.yolo === true,
17257
17754
  systemPrompt,
17258
17755
  prompt: input.launchMode === "fresh" ? input.prompt : undefined,
@@ -17377,6 +17874,7 @@ class LifecycleService {
17377
17874
  const baseBranch = input.mode === "new" ? requestedBaseBranch || this.deps.config.workspace.mainBranch : undefined;
17378
17875
  const branchAvailability = this.resolveBranchAvailability(input.branch, input.mode);
17379
17876
  const { profileName, profile } = this.resolveProfile(input.profile);
17877
+ const agent = this.resolveAgentDefinition(input.agent);
17380
17878
  const worktreePath = this.resolveWorktreePath(input.branch);
17381
17879
  const createProgressBase = {
17382
17880
  branch: input.branch,
@@ -17401,7 +17899,7 @@ class LifecycleService {
17401
17899
  ...baseBranch ? { baseBranch } : {},
17402
17900
  ...branchAvailability.startPoint ? { startPoint: branchAvailability.startPoint } : {},
17403
17901
  profile: profileName,
17404
- agent: input.agent,
17902
+ agent: agent.id,
17405
17903
  runtime: profile.runtime,
17406
17904
  startupEnvValues: await this.buildStartupEnvValues(input.envOverrides),
17407
17905
  allocatedPorts: await this.allocatePorts(),
@@ -17441,8 +17939,9 @@ class LifecycleService {
17441
17939
  });
17442
17940
  await this.materializeRuntimeSession({
17443
17941
  branch: input.branch,
17942
+ profileName,
17444
17943
  profile,
17445
- agent: input.agent,
17944
+ agent,
17446
17945
  initialized,
17447
17946
  worktreePath,
17448
17947
  prompt: input.prompt,
@@ -17483,6 +17982,8 @@ var init_lifecycle_service = __esm(() => {
17483
17982
  init_config();
17484
17983
  init_tmux();
17485
17984
  init_policies();
17985
+ init_agent_service();
17986
+ init_agent_registry();
17486
17987
  init_session_service();
17487
17988
  init_worktree_service();
17488
17989
  init_log();
@@ -17778,9 +18279,9 @@ async function mapWithConcurrency(items, limit, fn) {
17778
18279
  }
17779
18280
 
17780
18281
  // backend/src/services/reconciliation-service.ts
17781
- import { basename as basename4, resolve as resolve8 } from "path";
18282
+ import { basename as basename4, resolve as resolve9 } from "path";
17782
18283
  function makeUnmanagedWorktreeId(path) {
17783
- return `unmanaged:${resolve8(path)}`;
18284
+ return `unmanaged:${resolve9(path)}`;
17784
18285
  }
17785
18286
  function isValidPort2(port) {
17786
18287
  return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
@@ -17837,7 +18338,7 @@ class ReconciliationService {
17837
18338
  if (!options.force && this.now() - this.lastReconciledAt < this.freshnessMs) {
17838
18339
  return;
17839
18340
  }
17840
- const normalizedRepoRoot = resolve8(repoRoot);
18341
+ const normalizedRepoRoot = resolve9(repoRoot);
17841
18342
  const reconcilePromise = this.runReconcile(normalizedRepoRoot).then(() => {
17842
18343
  this.lastReconciledAt = this.now();
17843
18344
  });
@@ -17856,7 +18357,7 @@ class ReconciliationService {
17856
18357
  windows = [];
17857
18358
  }
17858
18359
  const seenWorktreeIds = new Set;
17859
- const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve8(entry.path) !== normalizedRepoRoot);
18360
+ const candidateEntries = worktrees.filter((entry) => !entry.bare && resolve9(entry.path) !== normalizedRepoRoot);
17860
18361
  const reconciledStates = await mapWithConcurrency(candidateEntries, this.concurrency, async (entry) => {
17861
18362
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
17862
18363
  const meta = await readWorktreeMeta(gitDir);
@@ -18042,19 +18543,19 @@ __export(exports_worktree_commands, {
18042
18543
  parseAddCommandArgs: () => parseAddCommandArgs,
18043
18544
  getWorktreeCommandUsage: () => getWorktreeCommandUsage
18044
18545
  });
18045
- import { basename as basename5, resolve as resolve9 } from "path";
18546
+ import { basename as basename5, resolve as resolve10 } from "path";
18046
18547
  function getWorktreeCommandUsage(command) {
18047
18548
  switch (command) {
18048
18549
  case "add":
18049
18550
  return [
18050
18551
  "Usage:",
18051
- " webmux add [branch] [--existing] [--base <branch>] [--profile <name>] [--agent <claude|codex|both>] [--prompt <text>] [--env KEY=VALUE] [--detach]",
18552
+ " webmux add [branch] [--existing] [--base <branch>] [--profile <name>] [--agent <id>] [--prompt <text>] [--env KEY=VALUE] [--detach]",
18052
18553
  "",
18053
18554
  "Options:",
18054
18555
  " --existing Use an existing local or remote branch instead of creating a new one",
18055
18556
  " --base <branch> Base branch for a new worktree (defaults to config)",
18056
18557
  " --profile <name> Worktree profile from .webmux.yaml",
18057
- " --agent <claude|codex|both> Agent to launch (both creates paired worktrees)",
18558
+ " --agent <id> Agent id to launch (repeatable)",
18058
18559
  " --prompt <text> Initial agent prompt",
18059
18560
  " --env KEY=VALUE Runtime env override (repeatable)",
18060
18561
  " -d, --detach Create worktree without switching to it",
@@ -18129,14 +18630,16 @@ function readOptionValue(args, index, flag) {
18129
18630
  };
18130
18631
  }
18131
18632
  function parseAgent(value) {
18132
- if (value === "claude" || value === "codex" || value === "both") {
18133
- return value;
18633
+ const trimmed = value.trim();
18634
+ if (!trimmed) {
18635
+ throw new CommandUsageError("Agent id cannot be empty");
18134
18636
  }
18135
- throw new CommandUsageError(`Unknown agent: ${value}`);
18637
+ return trimmed;
18136
18638
  }
18137
18639
  function parseAddCommandArgs(args) {
18138
18640
  const input = {};
18139
18641
  const envOverrides = {};
18642
+ const selectedAgents = [];
18140
18643
  let detach = false;
18141
18644
  for (let index = 0;index < args.length; index++) {
18142
18645
  const arg = args[index];
@@ -18167,7 +18670,7 @@ function parseAddCommandArgs(args) {
18167
18670
  }
18168
18671
  if (arg === "--agent" || arg.startsWith("--agent=")) {
18169
18672
  const { value, nextIndex } = readOptionValue(args, index, "--agent");
18170
- input.agent = parseAgent(value);
18673
+ selectedAgents.push(parseAgent(value));
18171
18674
  index = nextIndex;
18172
18675
  continue;
18173
18676
  }
@@ -18195,6 +18698,9 @@ function parseAddCommandArgs(args) {
18195
18698
  }
18196
18699
  input.branch = arg;
18197
18700
  }
18701
+ if (selectedAgents.length > 0) {
18702
+ input.agents = selectedAgents;
18703
+ }
18198
18704
  if (Object.keys(envOverrides).length > 0) {
18199
18705
  input.envOverrides = envOverrides;
18200
18706
  }
@@ -18318,8 +18824,8 @@ function parseListCommandArgs(args) {
18318
18824
  return { mode, search };
18319
18825
  }
18320
18826
  function listProjectWorktrees(runtime) {
18321
- const projectDir = resolve9(runtime.projectDir);
18322
- return runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve9(entry.path) !== projectDir);
18827
+ const projectDir = resolve10(runtime.projectDir);
18828
+ return runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve10(entry.path) !== projectDir);
18323
18829
  }
18324
18830
  async function defaultConfirmPrune(worktreeCount) {
18325
18831
  const response = await ot2({
@@ -18329,7 +18835,7 @@ async function defaultConfirmPrune(worktreeCount) {
18329
18835
  return !q(response) && response;
18330
18836
  }
18331
18837
  function defaultSwitchToTmuxWindow(projectDir, branch) {
18332
- const sessionName = buildProjectSessionName(resolve9(projectDir));
18838
+ const sessionName = buildProjectSessionName(resolve10(projectDir));
18333
18839
  const windowName = buildWorktreeWindowName(branch);
18334
18840
  const target = `${sessionName}:${windowName}`;
18335
18841
  const selectResult = Bun.spawnSync(["tmux", "select-window", "-t", target], {
@@ -18361,7 +18867,7 @@ function matchesListSearch(row, query) {
18361
18867
  return query.length === 0 || row.searchText.toLowerCase().includes(query.toLowerCase());
18362
18868
  }
18363
18869
  async function listWorktrees(runtime, stdout, options) {
18364
- const projectDir = resolve9(runtime.projectDir);
18870
+ const projectDir = resolve10(runtime.projectDir);
18365
18871
  const entries = listProjectWorktrees(runtime);
18366
18872
  if (entries.length === 0) {
18367
18873
  stdout("No worktrees found.");
@@ -18386,7 +18892,7 @@ async function listWorktrees(runtime, stdout, options) {
18386
18892
  return {
18387
18893
  branch,
18388
18894
  isOpen,
18389
- archived: archivedPaths.has(resolve9(entry.path)),
18895
+ archived: archivedPaths.has(resolve10(entry.path)),
18390
18896
  info,
18391
18897
  searchText: [
18392
18898
  branch,
@@ -18452,21 +18958,12 @@ async function runWorktreeCommand(context, deps2 = {}) {
18452
18958
  if (!parsed.input.branch && parsed.input.prompt && runtime2.config.autoName) {
18453
18959
  stdout("Generating branch name...");
18454
18960
  }
18455
- if (parsed.input.agent === "both") {
18456
- const result = await runtime2.lifecycleService.createWorktrees(parsed.input);
18457
- for (const branch2 of result.branches) {
18458
- stdout(`Created worktree ${branch2}`);
18459
- }
18460
- if (!parsed.detach) {
18461
- switchToTmuxWindow(runtime2.projectDir, result.primaryBranch);
18462
- }
18463
- } else {
18464
- const { agent, ...rest } = parsed.input;
18465
- const result = await runtime2.lifecycleService.createWorktree({ ...rest, agent });
18466
- stdout(`Created worktree ${result.branch}`);
18467
- if (!parsed.detach) {
18468
- switchToTmuxWindow(runtime2.projectDir, result.branch);
18469
- }
18961
+ const result = await runtime2.lifecycleService.createWorktrees(parsed.input);
18962
+ for (const branch2 of result.branches) {
18963
+ stdout(`Created worktree ${branch2}`);
18964
+ }
18965
+ if (!parsed.detach) {
18966
+ switchToTmuxWindow(runtime2.projectDir, result.primaryBranch);
18470
18967
  }
18471
18968
  return 0;
18472
18969
  }
@@ -18599,13 +19096,13 @@ var init_worktree_commands = __esm(() => {
18599
19096
  });
18600
19097
 
18601
19098
  // bin/src/webmux.ts
18602
- import { resolve as resolve10, dirname as dirname6, join as join11 } from "path";
19099
+ import { resolve as resolve11, dirname as dirname6, join as join11 } from "path";
18603
19100
  import { existsSync as existsSync5 } from "fs";
18604
19101
  import { fileURLToPath } from "url";
18605
19102
  // package.json
18606
19103
  var package_default = {
18607
19104
  name: "webmux",
18608
- version: "0.28.1",
19105
+ version: "0.30.0",
18609
19106
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
18610
19107
  type: "module",
18611
19108
  repository: {
@@ -18629,9 +19126,7 @@ var package_default = {
18629
19126
  "frontend",
18630
19127
  "packages/*"
18631
19128
  ],
18632
- dependencies: {
18633
- "@webmux/api-contract": "workspace:*"
18634
- },
19129
+ dependencies: {},
18635
19130
  scripts: {
18636
19131
  dev: "bash dev.sh",
18637
19132
  start: "bun bin/webmux.js",
@@ -18646,6 +19141,7 @@ var package_default = {
18646
19141
  "frontend/dist/"
18647
19142
  ],
18648
19143
  devDependencies: {
19144
+ "@webmux/api-contract": "workspace:*",
18649
19145
  "@clack/prompts": "^1.1.0",
18650
19146
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
18651
19147
  "@tailwindcss/vite": "^4.2.0",
@@ -18663,7 +19159,7 @@ var package_default = {
18663
19159
  };
18664
19160
 
18665
19161
  // bin/src/webmux.ts
18666
- var PKG_ROOT = resolve10(dirname6(fileURLToPath(import.meta.url)), "..");
19162
+ var PKG_ROOT = resolve11(dirname6(fileURLToPath(import.meta.url)), "..");
18667
19163
  function usage2() {
18668
19164
  console.log(`
18669
19165
  webmux \u2014 Dev dashboard for managing Git worktrees
@@ -18878,8 +19374,8 @@ async function main(args = process.argv.slice(2)) {
18878
19374
  const code = await proc.exited;
18879
19375
  process.exit(code);
18880
19376
  }
18881
- await loadEnvFile(resolve10(process.cwd(), ".env.local"));
18882
- await loadEnvFile(resolve10(process.cwd(), ".env"));
19377
+ await loadEnvFile(resolve11(process.cwd(), ".env.local"));
19378
+ await loadEnvFile(resolve11(process.cwd(), ".env"));
18883
19379
  if (isWorktreeCommand(parsed.command)) {
18884
19380
  const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
18885
19381
  const exitCode2 = await runWorktreeCommand2({
@@ -18894,7 +19390,7 @@ async function main(args = process.argv.slice(2)) {
18894
19390
  usage2();
18895
19391
  process.exit(0);
18896
19392
  }
18897
- if (!existsSync5(resolve10(process.cwd(), ".webmux.yaml"))) {
19393
+ if (!existsSync5(resolve11(process.cwd(), ".webmux.yaml"))) {
18898
19394
  console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
18899
19395
  process.exit(1);
18900
19396
  }