webmux 0.23.0 → 0.24.1

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.
@@ -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,7 @@ 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";
10852
10918
  var DEFAULT_SYSTEM_PROMPT = [
10853
10919
  "Generate a concise git branch name from the task description.",
10854
10920
  "Return only the branch name.",
@@ -10899,11 +10965,12 @@ function buildClaudeArgs(model, systemPrompt, prompt) {
10899
10965
  systemPrompt,
10900
10966
  "--output-format",
10901
10967
  "text",
10902
- "--no-session-persistence"
10968
+ "--no-session-persistence",
10969
+ "--model",
10970
+ model || DEFAULT_AUTO_NAME_MODEL,
10971
+ "--effort",
10972
+ "low"
10903
10973
  ];
10904
- if (model) {
10905
- args.push("--model", model);
10906
- }
10907
10974
  args.push(prompt);
10908
10975
  return args;
10909
10976
  }
@@ -10949,8 +11016,11 @@ class AutoNameService {
10949
11016
  throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
10950
11017
  }
10951
11018
  if (result.exitCode !== 0) {
10952
- const detail = result.stderr.trim() || `exit ${result.exitCode}`;
10953
- throw new Error(`${cli} failed: ${detail}`);
11019
+ const stderr = result.stderr.trim();
11020
+ const stdout = result.stdout.trim();
11021
+ const output2 = stderr || stdout || `exit ${result.exitCode}`;
11022
+ const command = args.join(" ");
11023
+ throw new Error(`${cli} failed (command: ${command}): ${output2}`);
10954
11024
  }
10955
11025
  const output = result.stdout.trim();
10956
11026
  if (!output) {
@@ -11770,13 +11840,17 @@ async function apiCreateWorktree(req) {
11770
11840
  const baseBranch = typeof body.baseBranch === "string" && body.baseBranch.trim() ? body.baseBranch.trim() : undefined;
11771
11841
  const prompt = typeof body.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : undefined;
11772
11842
  const profile = typeof body.profile === "string" ? body.profile : undefined;
11773
- const agent = body.agent === "claude" || body.agent === "codex" ? body.agent : undefined;
11843
+ const agent = body.agent === "claude" || body.agent === "codex" || body.agent === "both" ? body.agent : undefined;
11774
11844
  const createLinearTicket = body.createLinearTicket === true;
11775
11845
  const linearTitle = typeof body.linearTitle === "string" && body.linearTitle.trim() ? body.linearTitle.trim() : undefined;
11776
11846
  const mode = body.mode === "new" || body.mode === "existing" ? body.mode : undefined;
11847
+ const agentSelection = agent ?? config.workspace.defaultAgent;
11777
11848
  if (body.mode !== undefined && body.mode !== "new" && body.mode !== "existing") {
11778
11849
  return errorResponse("Invalid worktree create mode", 400);
11779
11850
  }
11851
+ if (body.agent !== undefined && body.agent !== "claude" && body.agent !== "codex" && body.agent !== "both") {
11852
+ return errorResponse("Invalid agent selection", 400);
11853
+ }
11780
11854
  if (baseBranch && !isValidBranchName(baseBranch)) {
11781
11855
  return errorResponse("Invalid base branch name", 400);
11782
11856
  }
@@ -11814,26 +11888,32 @@ async function apiCreateWorktree(req) {
11814
11888
  return errorResponse(linearResult.error, 502);
11815
11889
  }
11816
11890
  resolvedBranch = linearResult.data.branchName;
11817
- ensureBranchNotCreating(resolvedBranch);
11818
11891
  log.info(`[linear] created ticket ${linearResult.data.identifier} branch=${linearResult.data.branchName} title="${linearResult.data.title.slice(0, 80)}"`);
11819
- } else if (resolvedBranch) {
11820
- ensureBranchNotCreating(resolvedBranch);
11821
11892
  }
11822
- if (resolvedBranch && baseBranch && resolvedBranch === baseBranch) {
11823
- return errorResponse("Base branch must differ from branch name", 400);
11893
+ if (resolvedBranch) {
11894
+ const targetBranches = buildCreateWorktreeTargets(resolvedBranch, agentSelection).map((target) => target.branch);
11895
+ for (const targetBranch of targetBranches) {
11896
+ ensureBranchNotBusy(targetBranch);
11897
+ }
11898
+ if (baseBranch && targetBranches.some((targetBranch) => targetBranch === baseBranch)) {
11899
+ return errorResponse("Base branch must differ from branch name", 400);
11900
+ }
11824
11901
  }
11825
- 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)}"` : ""}`);
11826
- const result = await lifecycleService.createWorktree({
11902
+ 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)}"` : ""}`);
11903
+ const result = await lifecycleService.createWorktrees({
11827
11904
  mode,
11828
11905
  branch: resolvedBranch,
11829
11906
  baseBranch,
11830
11907
  prompt,
11831
11908
  profile,
11832
- agent,
11909
+ agent: agentSelection,
11833
11910
  envOverrides
11834
11911
  });
11835
- log.debug(`[worktree:add] done branch=${result.branch} worktreeId=${result.worktreeId}`);
11836
- return jsonResponse({ branch: result.branch }, 201);
11912
+ log.debug(`[worktree:add] done branches=${result.branches.join(",")}`);
11913
+ return jsonResponse({
11914
+ primaryBranch: result.primaryBranch,
11915
+ branches: result.branches
11916
+ }, 201);
11837
11917
  }
11838
11918
  async function apiDeleteWorktree(name) {
11839
11919
  return withRemovingBranch(name, async () => {
@@ -11959,11 +12039,13 @@ async function apiGetWorktreeDiff(name) {
11959
12039
  if (!state)
11960
12040
  return errorResponse(`Worktree not found: ${name}`, 404);
11961
12041
  const uncommitted = git.readDiff(state.path);
12042
+ const gitStatus = git.readStatus(state.path);
11962
12043
  const unpushedCommits = git.listUnpushedCommits(state.path);
11963
12044
  const truncated = uncommitted.length > MAX_DIFF_BYTES;
11964
12045
  return jsonResponse({
11965
12046
  uncommitted: truncated ? uncommitted.slice(0, MAX_DIFF_BYTES) : uncommitted,
11966
12047
  uncommittedTruncated: truncated,
12048
+ gitStatus,
11967
12049
  unpushedCommits
11968
12050
  });
11969
12051
  }