webmux 0.22.1 → 0.24.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.
@@ -8630,7 +8630,7 @@ function buildAgentInvocation(input) {
8630
8630
  if (input.launchMode === "resume") {
8631
8631
  return `codex${yoloFlag2} resume --last`;
8632
8632
  }
8633
- const promptSuffix2 = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
8633
+ const promptSuffix2 = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
8634
8634
  if (input.systemPrompt) {
8635
8635
  return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
8636
8636
  }
@@ -8640,7 +8640,7 @@ function buildAgentInvocation(input) {
8640
8640
  if (input.launchMode === "resume") {
8641
8641
  return `claude${yoloFlag} --continue`;
8642
8642
  }
8643
- const promptSuffix = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
8643
+ const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
8644
8644
  if (input.systemPrompt) {
8645
8645
  return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
8646
8646
  }
@@ -8968,6 +8968,9 @@ class BunGitGateway {
8968
8968
  readWorktreeStatus(cwd) {
8969
8969
  return readGitWorktreeStatus(cwd);
8970
8970
  }
8971
+ readStatus(cwd) {
8972
+ return runGit(["status", "--short", "--untracked-files=all"], cwd);
8973
+ }
8971
8974
  createWorktree(opts) {
8972
8975
  const args = ["worktree", "add"];
8973
8976
  if (opts.mode === "new") {
@@ -9211,6 +9214,18 @@ function toErrorMessage2(error) {
9211
9214
  function stringifyStartupEnvValue(value) {
9212
9215
  return typeof value === "boolean" ? String(value) : value;
9213
9216
  }
9217
+ function prefixAgentBranch(agent, branch) {
9218
+ return `${agent}-${branch}`;
9219
+ }
9220
+ function buildCreateWorktreeTargets(branch, agentSelection) {
9221
+ if (agentSelection === "both") {
9222
+ return [
9223
+ { branch: prefixAgentBranch("claude", branch), agent: "claude" },
9224
+ { branch: prefixAgentBranch("codex", branch), agent: "codex" }
9225
+ ];
9226
+ }
9227
+ return [{ branch, agent: agentSelection }];
9228
+ }
9214
9229
 
9215
9230
  class LifecycleError extends Error {
9216
9231
  status;
@@ -9225,114 +9240,47 @@ class LifecycleService {
9225
9240
  constructor(deps) {
9226
9241
  this.deps = deps;
9227
9242
  }
9228
- async createWorktree(input) {
9243
+ async createWorktrees(input) {
9229
9244
  const mode = input.mode ?? "new";
9230
- const requestedBaseBranch = input.baseBranch?.trim();
9231
- if (requestedBaseBranch && !isValidBranchName(requestedBaseBranch)) {
9232
- throw new LifecycleError("Invalid base branch name", 400);
9233
- }
9234
- if (requestedBaseBranch && mode === "existing") {
9235
- throw new LifecycleError("Base branch is only supported for new worktrees", 400);
9245
+ const agentSelection = input.agent ?? this.deps.config.workspace.defaultAgent;
9246
+ if (agentSelection === "both" && mode === "existing") {
9247
+ throw new LifecycleError("Creating both agents is only supported for new worktrees", 400);
9236
9248
  }
9237
9249
  const branch = await this.resolveBranch(input.branch, input.prompt, mode);
9238
- if (requestedBaseBranch && requestedBaseBranch === branch) {
9239
- throw new LifecycleError("Base branch must differ from branch name", 400);
9240
- }
9241
- const baseBranch = mode === "new" ? requestedBaseBranch || this.deps.config.workspace.mainBranch : undefined;
9242
- const branchAvailability = this.resolveBranchAvailability(branch, mode);
9243
- const { profileName, profile } = this.resolveProfile(input.profile);
9244
- const agent = this.resolveAgent(input.agent);
9245
- const worktreePath = this.resolveWorktreePath(branch);
9246
- const createProgressBase = {
9247
- branch,
9248
- ...baseBranch ? { baseBranch } : {},
9249
- path: worktreePath,
9250
- profile: profileName,
9251
- agent
9252
- };
9253
- const deleteBranchOnRollback = mode === "new" || branchAvailability.deleteBranchOnRollback;
9254
- let initialized = null;
9250
+ const targets = buildCreateWorktreeTargets(branch, agentSelection);
9251
+ const createdBranches = [];
9255
9252
  try {
9256
- await this.reportCreateProgress({
9257
- ...createProgressBase,
9258
- phase: "creating_worktree"
9259
- });
9260
- await mkdir4(dirname4(worktreePath), { recursive: true });
9261
- initialized = await createManagedWorktree({
9262
- repoRoot: this.deps.projectRoot,
9263
- worktreePath,
9264
- branch,
9265
- mode,
9266
- ...baseBranch ? { baseBranch } : {},
9267
- ...branchAvailability.startPoint ? { startPoint: branchAvailability.startPoint } : {},
9268
- profile: profileName,
9269
- agent,
9270
- runtime: profile.runtime,
9271
- startupEnvValues: await this.buildStartupEnvValues(input.envOverrides),
9272
- allocatedPorts: await this.allocatePorts(),
9273
- runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
9274
- controlUrl: this.controlUrl(),
9275
- controlToken: await this.deps.getControlToken(),
9276
- deleteBranchOnRollback
9277
- }, {
9278
- git: this.deps.git
9279
- });
9280
- await this.reportCreateProgress({
9281
- ...createProgressBase,
9282
- phase: "running_post_create_hook"
9283
- });
9284
- await this.runLifecycleHook({
9285
- name: "postCreate",
9286
- command: this.deps.config.lifecycleHooks.postCreate,
9287
- meta: initialized.meta,
9288
- worktreePath
9289
- });
9290
- initialized = await this.refreshManagedArtifactsFromMeta({
9291
- gitDir: initialized.paths.gitDir,
9292
- meta: initialized.meta,
9293
- worktreePath
9294
- });
9295
- await this.reportCreateProgress({
9296
- ...createProgressBase,
9297
- phase: "preparing_runtime"
9298
- });
9299
- await ensureAgentRuntimeArtifacts({
9300
- gitDir: initialized.paths.gitDir,
9301
- worktreePath
9302
- });
9303
- await this.reportCreateProgress({
9304
- ...createProgressBase,
9305
- phase: "starting_session"
9306
- });
9307
- await this.materializeRuntimeSession({
9308
- branch,
9309
- profile,
9310
- agent,
9311
- initialized,
9312
- worktreePath,
9313
- prompt: input.prompt,
9314
- launchMode: "fresh"
9315
- });
9316
- await this.reportCreateProgress({
9317
- ...createProgressBase,
9318
- phase: "reconciling"
9319
- });
9320
- await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
9321
- return {
9322
- branch,
9323
- worktreeId: initialized.meta.worktreeId
9324
- };
9253
+ for (const target of targets) {
9254
+ const created = await this.createResolvedWorktree({
9255
+ ...input,
9256
+ mode,
9257
+ branch: target.branch,
9258
+ agent: target.agent
9259
+ });
9260
+ createdBranches.push(created.branch);
9261
+ }
9325
9262
  } catch (error) {
9326
- if (initialized) {
9327
- const cleanupError = await this.cleanupFailedCreate(branch, worktreePath, profile.runtime, deleteBranchOnRollback);
9328
- if (cleanupError) {
9329
- throw this.wrapOperationError(new Error(`${toErrorMessage2(error)}; ${cleanupError}`));
9330
- }
9263
+ const rollbackError = await this.rollbackCreatedWorktrees(createdBranches);
9264
+ if (rollbackError) {
9265
+ throw this.wrapOperationError(new Error(`${toErrorMessage2(error)}; ${rollbackError}`));
9331
9266
  }
9332
9267
  throw this.wrapOperationError(error);
9333
- } finally {
9334
- await this.finishCreateProgress(branch);
9335
9268
  }
9269
+ return {
9270
+ primaryBranch: createdBranches[0],
9271
+ branches: createdBranches
9272
+ };
9273
+ }
9274
+ async createWorktree(input) {
9275
+ const mode = input.mode ?? "new";
9276
+ const branch = await this.resolveBranch(input.branch, input.prompt, mode);
9277
+ const agent = this.resolveAgent(input.agent);
9278
+ return await this.createResolvedWorktree({
9279
+ ...input,
9280
+ mode,
9281
+ branch,
9282
+ agent
9283
+ });
9336
9284
  }
9337
9285
  async openWorktree(branch) {
9338
9286
  try {
@@ -9733,6 +9681,123 @@ class LifecycleService {
9733
9681
  async finishCreateProgress(branch) {
9734
9682
  await this.deps.onCreateFinished?.(branch);
9735
9683
  }
9684
+ async rollbackCreatedWorktrees(branches) {
9685
+ const cleanupErrors = [];
9686
+ for (const branch of [...branches].reverse()) {
9687
+ try {
9688
+ await this.removeWorktree(branch);
9689
+ } catch (error) {
9690
+ cleanupErrors.push(`rollback failed for ${branch}: ${toErrorMessage2(error)}`);
9691
+ }
9692
+ }
9693
+ return cleanupErrors.length > 0 ? cleanupErrors.join("; ") : null;
9694
+ }
9695
+ async createResolvedWorktree(input) {
9696
+ const requestedBaseBranch = input.baseBranch?.trim();
9697
+ if (requestedBaseBranch && !isValidBranchName(requestedBaseBranch)) {
9698
+ throw new LifecycleError("Invalid base branch name", 400);
9699
+ }
9700
+ if (requestedBaseBranch && input.mode === "existing") {
9701
+ throw new LifecycleError("Base branch is only supported for new worktrees", 400);
9702
+ }
9703
+ if (requestedBaseBranch && requestedBaseBranch === input.branch) {
9704
+ throw new LifecycleError("Base branch must differ from branch name", 400);
9705
+ }
9706
+ const baseBranch = input.mode === "new" ? requestedBaseBranch || this.deps.config.workspace.mainBranch : undefined;
9707
+ const branchAvailability = this.resolveBranchAvailability(input.branch, input.mode);
9708
+ const { profileName, profile } = this.resolveProfile(input.profile);
9709
+ const worktreePath = this.resolveWorktreePath(input.branch);
9710
+ const createProgressBase = {
9711
+ branch: input.branch,
9712
+ ...baseBranch ? { baseBranch } : {},
9713
+ path: worktreePath,
9714
+ profile: profileName,
9715
+ agent: input.agent
9716
+ };
9717
+ const deleteBranchOnRollback = input.mode === "new" || branchAvailability.deleteBranchOnRollback;
9718
+ let initialized = null;
9719
+ try {
9720
+ await this.reportCreateProgress({
9721
+ ...createProgressBase,
9722
+ phase: "creating_worktree"
9723
+ });
9724
+ await mkdir4(dirname4(worktreePath), { recursive: true });
9725
+ initialized = await createManagedWorktree({
9726
+ repoRoot: this.deps.projectRoot,
9727
+ worktreePath,
9728
+ branch: input.branch,
9729
+ mode: input.mode,
9730
+ ...baseBranch ? { baseBranch } : {},
9731
+ ...branchAvailability.startPoint ? { startPoint: branchAvailability.startPoint } : {},
9732
+ profile: profileName,
9733
+ agent: input.agent,
9734
+ runtime: profile.runtime,
9735
+ startupEnvValues: await this.buildStartupEnvValues(input.envOverrides),
9736
+ allocatedPorts: await this.allocatePorts(),
9737
+ runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
9738
+ controlUrl: this.controlUrl(),
9739
+ controlToken: await this.deps.getControlToken(),
9740
+ deleteBranchOnRollback
9741
+ }, {
9742
+ git: this.deps.git
9743
+ });
9744
+ await this.reportCreateProgress({
9745
+ ...createProgressBase,
9746
+ phase: "running_post_create_hook"
9747
+ });
9748
+ await this.runLifecycleHook({
9749
+ name: "postCreate",
9750
+ command: this.deps.config.lifecycleHooks.postCreate,
9751
+ meta: initialized.meta,
9752
+ worktreePath
9753
+ });
9754
+ initialized = await this.refreshManagedArtifactsFromMeta({
9755
+ gitDir: initialized.paths.gitDir,
9756
+ meta: initialized.meta,
9757
+ worktreePath
9758
+ });
9759
+ await this.reportCreateProgress({
9760
+ ...createProgressBase,
9761
+ phase: "preparing_runtime"
9762
+ });
9763
+ await ensureAgentRuntimeArtifacts({
9764
+ gitDir: initialized.paths.gitDir,
9765
+ worktreePath
9766
+ });
9767
+ await this.reportCreateProgress({
9768
+ ...createProgressBase,
9769
+ phase: "starting_session"
9770
+ });
9771
+ await this.materializeRuntimeSession({
9772
+ branch: input.branch,
9773
+ profile,
9774
+ agent: input.agent,
9775
+ initialized,
9776
+ worktreePath,
9777
+ prompt: input.prompt,
9778
+ launchMode: "fresh"
9779
+ });
9780
+ await this.reportCreateProgress({
9781
+ ...createProgressBase,
9782
+ phase: "reconciling"
9783
+ });
9784
+ await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
9785
+ return {
9786
+ branch: input.branch,
9787
+ worktreeId: initialized.meta.worktreeId
9788
+ };
9789
+ } catch (error) {
9790
+ if (initialized) {
9791
+ const cleanupError = await this.cleanupFailedCreate(input.branch, worktreePath, profile.runtime, deleteBranchOnRollback);
9792
+ if (cleanupError) {
9793
+ throw this.wrapOperationError(new Error(`${toErrorMessage2(error)}; ${cleanupError}`));
9794
+ }
9795
+ }
9796
+ throw this.wrapOperationError(error);
9797
+ } finally {
9798
+ await this.finishCreateProgress(input.branch);
9799
+ }
9800
+ }
9736
9801
  wrapOperationError(error) {
9737
9802
  if (error instanceof LifecycleError) {
9738
9803
  return error;
@@ -10849,6 +10914,8 @@ class BunPortProbe {
10849
10914
 
10850
10915
  // backend/src/services/auto-name-service.ts
10851
10916
  var MAX_BRANCH_LENGTH = 40;
10917
+ var DEFAULT_AUTO_NAME_MODEL = "claude-haiku-4-5-20251001";
10918
+ var AUTO_NAME_MAX_TOKENS = 50;
10852
10919
  var DEFAULT_SYSTEM_PROMPT = [
10853
10920
  "Generate a concise git branch name from the task description.",
10854
10921
  "Return only the branch name.",
@@ -10899,11 +10966,12 @@ function buildClaudeArgs(model, systemPrompt, prompt) {
10899
10966
  systemPrompt,
10900
10967
  "--output-format",
10901
10968
  "text",
10902
- "--no-session-persistence"
10969
+ "--no-session-persistence",
10970
+ "--model",
10971
+ model || DEFAULT_AUTO_NAME_MODEL,
10972
+ "--max-tokens",
10973
+ String(AUTO_NAME_MAX_TOKENS)
10903
10974
  ];
10904
- if (model) {
10905
- args.push("--model", model);
10906
- }
10907
10975
  args.push(prompt);
10908
10976
  return args;
10909
10977
  }
@@ -10949,8 +11017,11 @@ class AutoNameService {
10949
11017
  throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
10950
11018
  }
10951
11019
  if (result.exitCode !== 0) {
10952
- const detail = result.stderr.trim() || `exit ${result.exitCode}`;
10953
- throw new Error(`${cli} failed: ${detail}`);
11020
+ const stderr = result.stderr.trim();
11021
+ const stdout = result.stdout.trim();
11022
+ const output2 = stderr || stdout || `exit ${result.exitCode}`;
11023
+ const command = args.join(" ");
11024
+ throw new Error(`${cli} failed (command: ${command}): ${output2}`);
10954
11025
  }
10955
11026
  const output = result.stdout.trim();
10956
11027
  if (!output) {
@@ -11433,6 +11504,7 @@ function createWebmuxRuntime(options = {}) {
11433
11504
  autoName,
11434
11505
  onCreateProgress: (progress) => {
11435
11506
  worktreeCreationTracker.set(progress);
11507
+ options.onCreateProgress?.(progress);
11436
11508
  },
11437
11509
  onCreateFinished: (branch) => {
11438
11510
  worktreeCreationTracker.clear(branch);
@@ -11769,13 +11841,17 @@ async function apiCreateWorktree(req) {
11769
11841
  const baseBranch = typeof body.baseBranch === "string" && body.baseBranch.trim() ? body.baseBranch.trim() : undefined;
11770
11842
  const prompt = typeof body.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : undefined;
11771
11843
  const profile = typeof body.profile === "string" ? body.profile : undefined;
11772
- const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
11844
+ const agent = body.agent === "claude" || body.agent === "codex" || body.agent === "both" ? body.agent : undefined;
11773
11845
  const createLinearTicket = body.createLinearTicket === true;
11774
11846
  const linearTitle = typeof body.linearTitle === "string" && body.linearTitle.trim() ? body.linearTitle.trim() : undefined;
11775
11847
  const mode = body.mode === "new" || body.mode === "existing" ? body.mode : undefined;
11848
+ const agentSelection = agent ?? config.workspace.defaultAgent;
11776
11849
  if (body.mode !== undefined && body.mode !== "new" && body.mode !== "existing") {
11777
11850
  return errorResponse("Invalid worktree create mode", 400);
11778
11851
  }
11852
+ if (body.agent !== undefined && body.agent !== "claude" && body.agent !== "codex" && body.agent !== "both") {
11853
+ return errorResponse("Invalid agent selection", 400);
11854
+ }
11779
11855
  if (baseBranch && !isValidBranchName(baseBranch)) {
11780
11856
  return errorResponse("Invalid base branch name", 400);
11781
11857
  }
@@ -11813,26 +11889,32 @@ async function apiCreateWorktree(req) {
11813
11889
  return errorResponse(linearResult.error, 502);
11814
11890
  }
11815
11891
  resolvedBranch = linearResult.data.branchName;
11816
- ensureBranchNotCreating(resolvedBranch);
11817
11892
  log.info(`[linear] created ticket ${linearResult.data.identifier} branch=${linearResult.data.branchName} title="${linearResult.data.title.slice(0, 80)}"`);
11818
- } else if (resolvedBranch) {
11819
- ensureBranchNotCreating(resolvedBranch);
11820
11893
  }
11821
- if (resolvedBranch && baseBranch && resolvedBranch === baseBranch) {
11822
- return errorResponse("Base branch must differ from branch name", 400);
11894
+ if (resolvedBranch) {
11895
+ const targetBranches = buildCreateWorktreeTargets(resolvedBranch, agentSelection).map((target) => target.branch);
11896
+ for (const targetBranch of targetBranches) {
11897
+ ensureBranchNotBusy(targetBranch);
11898
+ }
11899
+ if (baseBranch && targetBranches.some((targetBranch) => targetBranch === baseBranch)) {
11900
+ return errorResponse("Base branch must differ from branch name", 400);
11901
+ }
11823
11902
  }
11824
- log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${baseBranch ? ` base=${baseBranch}` : ""}${profile ? ` profile=${profile}` : ""}${agent ? ` agent=${agent}` : ""}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
11825
- const result = await lifecycleService.createWorktree({
11903
+ log.info(`[worktree:add] mode=${mode ?? "new"}${resolvedBranch ? ` branch=${resolvedBranch}` : ""}${baseBranch ? ` base=${baseBranch}` : ""}${profile ? ` profile=${profile}` : ""} agent=${agentSelection}${createLinearTicket ? " linearTicket=true" : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
11904
+ const result = await lifecycleService.createWorktrees({
11826
11905
  mode,
11827
11906
  branch: resolvedBranch,
11828
11907
  baseBranch,
11829
11908
  prompt,
11830
11909
  profile,
11831
- agent,
11910
+ agent: agentSelection,
11832
11911
  envOverrides
11833
11912
  });
11834
- log.debug(`[worktree:add] done branch=${result.branch} worktreeId=${result.worktreeId}`);
11835
- return jsonResponse({ branch: result.branch }, 201);
11913
+ log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
11914
+ return jsonResponse({
11915
+ primaryBranch: result.primaryBranch,
11916
+ branches: result.branches
11917
+ }, 201);
11836
11918
  }
11837
11919
  async function apiDeleteWorktree(name) {
11838
11920
  return withRemovingBranch(name, async () => {
@@ -11958,11 +12040,13 @@ async function apiGetWorktreeDiff(name) {
11958
12040
  if (!state)
11959
12041
  return errorResponse(`Worktree not found: ${name}`, 404);
11960
12042
  const uncommitted = git.readDiff(state.path);
12043
+ const gitStatus = git.readStatus(state.path);
11961
12044
  const unpushedCommits = git.listUnpushedCommits(state.path);
11962
12045
  const truncated = uncommitted.length > MAX_DIFF_BYTES;
11963
12046
  return jsonResponse({
11964
12047
  uncommitted: truncated ? uncommitted.slice(0, MAX_DIFF_BYTES) : uncommitted,
11965
12048
  uncommittedTruncated: truncated,
12049
+ gitStatus,
11966
12050
  unpushedCommits
11967
12051
  });
11968
12052
  }