webmux 0.23.0 → 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.
@@ -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) {
@@ -11770,13 +11841,17 @@ async function apiCreateWorktree(req) {
11770
11841
  const baseBranch = typeof body.baseBranch === "string" && body.baseBranch.trim() ? body.baseBranch.trim() : undefined;
11771
11842
  const prompt = typeof body.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : undefined;
11772
11843
  const profile = typeof body.profile === "string" ? body.profile : undefined;
11773
- 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;
11774
11845
  const createLinearTicket = body.createLinearTicket === true;
11775
11846
  const linearTitle = typeof body.linearTitle === "string" && body.linearTitle.trim() ? body.linearTitle.trim() : undefined;
11776
11847
  const mode = body.mode === "new" || body.mode === "existing" ? body.mode : undefined;
11848
+ const agentSelection = agent ?? config.workspace.defaultAgent;
11777
11849
  if (body.mode !== undefined && body.mode !== "new" && body.mode !== "existing") {
11778
11850
  return errorResponse("Invalid worktree create mode", 400);
11779
11851
  }
11852
+ if (body.agent !== undefined && body.agent !== "claude" && body.agent !== "codex" && body.agent !== "both") {
11853
+ return errorResponse("Invalid agent selection", 400);
11854
+ }
11780
11855
  if (baseBranch && !isValidBranchName(baseBranch)) {
11781
11856
  return errorResponse("Invalid base branch name", 400);
11782
11857
  }
@@ -11814,26 +11889,32 @@ async function apiCreateWorktree(req) {
11814
11889
  return errorResponse(linearResult.error, 502);
11815
11890
  }
11816
11891
  resolvedBranch = linearResult.data.branchName;
11817
- ensureBranchNotCreating(resolvedBranch);
11818
11892
  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
11893
  }
11822
- if (resolvedBranch && baseBranch && resolvedBranch === baseBranch) {
11823
- 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
+ }
11824
11902
  }
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({
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({
11827
11905
  mode,
11828
11906
  branch: resolvedBranch,
11829
11907
  baseBranch,
11830
11908
  prompt,
11831
11909
  profile,
11832
- agent,
11910
+ agent: agentSelection,
11833
11911
  envOverrides
11834
11912
  });
11835
- log.debug(`[worktree:add] done branch=${result.branch} worktreeId=${result.worktreeId}`);
11836
- 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);
11837
11918
  }
11838
11919
  async function apiDeleteWorktree(name) {
11839
11920
  return withRemovingBranch(name, async () => {
@@ -11959,11 +12040,13 @@ async function apiGetWorktreeDiff(name) {
11959
12040
  if (!state)
11960
12041
  return errorResponse(`Worktree not found: ${name}`, 404);
11961
12042
  const uncommitted = git.readDiff(state.path);
12043
+ const gitStatus = git.readStatus(state.path);
11962
12044
  const unpushedCommits = git.listUnpushedCommits(state.path);
11963
12045
  const truncated = uncommitted.length > MAX_DIFF_BYTES;
11964
12046
  return jsonResponse({
11965
12047
  uncommitted: truncated ? uncommitted.slice(0, MAX_DIFF_BYTES) : uncommitted,
11966
12048
  uncommittedTruncated: truncated,
12049
+ gitStatus,
11967
12050
  unpushedCommits
11968
12051
  });
11969
12052
  }