claude-team-mcp 0.6.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

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.
@@ -19,20 +19,10 @@ if TYPE_CHECKING:
19
19
  from ..cli_backends import get_cli_backend
20
20
  from ..colors import generate_tab_color
21
21
  from ..formatting import format_badge_text, format_session_title
22
- from ..iterm_utils import (
23
- MAX_PANES_PER_TAB,
24
- create_multi_pane_layout,
25
- find_available_window,
26
- get_window_for_session,
27
- send_prompt,
28
- send_prompt_for_agent,
29
- split_pane,
30
- start_agent_in_session,
31
- start_claude_in_session,
32
- )
33
22
  from ..names import pick_names_for_count
34
23
  from ..profile import apply_appearance_colors
35
24
  from ..registry import SessionStatus
25
+ from ..terminal_backends import ItermBackend, MAX_PANES_PER_TAB
36
26
  from ..utils import HINTS, error_response, get_worktree_tracker_dir
37
27
  from ..worker_prompt import generate_worker_prompt, get_coordinator_guidance
38
28
  from ..worktree import WorktreeError, create_local_worktree
@@ -40,6 +30,13 @@ from ..worktree import WorktreeError, create_local_worktree
40
30
  logger = logging.getLogger("claude-team-mcp")
41
31
 
42
32
 
33
+ class WorktreeConfig(TypedDict, total=False):
34
+ """Configuration for worktree creation."""
35
+
36
+ branch: str # Optional: Branch name for the worktree
37
+ base: str # Optional: Base ref/branch for the new branch
38
+
39
+
43
40
  class WorkerConfig(TypedDict, total=False):
44
41
  """Configuration for a single worker."""
45
42
 
@@ -51,6 +48,7 @@ class WorkerConfig(TypedDict, total=False):
51
48
  prompt: str # Optional: Custom prompt (None = standard worker prompt)
52
49
  skip_permissions: bool # Optional: Default False
53
50
  use_worktree: bool # Optional: Create isolated worktree (default True)
51
+ worktree: WorktreeConfig # Optional: Worktree settings (branch/base)
54
52
 
55
53
 
56
54
  def register_tools(mcp: FastMCP, ensure_connection) -> None:
@@ -65,9 +63,10 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
65
63
  """
66
64
  Spawn Claude Code worker sessions.
67
65
 
68
- Creates worker sessions in iTerm2, each with its own pane, Claude instance,
69
- and optional worktree. Workers can be spawned into existing windows (layout="auto")
70
- or a fresh window (layout="new").
66
+ Creates worker sessions in the active terminal backend, each with its own pane
67
+ (iTerm) or window (tmux), Claude instance, and optional worktree. Workers can be
68
+ spawned into existing
69
+ windows (layout="auto") or a fresh window (layout="new").
71
70
 
72
71
  **Layout Modes:**
73
72
 
@@ -83,6 +82,10 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
83
82
  - 3 workers: triple vertical (left/middle/right)
84
83
  - 4 workers: quad layout (2x2 grid)
85
84
 
85
+ **tmux note:**
86
+ - Workers always get their own tmux window in the shared "claude-team" session.
87
+ - layout is ignored for tmux.
88
+
86
89
  **WorkerConfig fields:**
87
90
  project_path: Required. Path to the repository.
88
91
  - Explicit path: Use this repo (e.g., "/path/to/repo")
@@ -107,6 +110,9 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
107
110
  - True: Creates worktree at <repo>/.worktrees/<bead>-<annotation>
108
111
  or <repo>/.worktrees/<name>-<uuid>-<annotation>
109
112
  - False: Worker uses the repo directory directly (no isolation)
113
+ worktree: Optional worktree configuration.
114
+ - branch: Branch name for the worktree
115
+ - base: Base ref/branch for the new branch
110
116
  name: Optional worker name override. Leaving this empty allows us to auto-pick names
111
117
  from themed sets (Beatles, Marx Brothers, etc.) which aids visual identification.
112
118
  annotation: Optional task description. Shown on badge second line, used in
@@ -180,8 +186,6 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
180
186
  # Then immediately:
181
187
  message_workers(session_ids=["Groucho"], message="Your task is...")
182
188
  """
183
- from iterm2.profile import LocalWriteOnlyProfile
184
-
185
189
  from ..session_state import await_marker_in_jsonl, generate_marker_message
186
190
 
187
191
  app_ctx = ctx.request_context.lifespan_context
@@ -201,8 +205,8 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
201
205
  if "project_path" not in w:
202
206
  return error_response(f"Worker {i} missing required 'project_path'")
203
207
 
204
- # Ensure we have a fresh connection
205
- connection, app = await ensure_connection(app_ctx)
208
+ # Ensure we have a fresh backend connection/state
209
+ backend = await ensure_connection(app_ctx)
206
210
 
207
211
  try:
208
212
  # Get base session index for color generation
@@ -233,6 +237,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
233
237
  resolved_paths: list[str] = []
234
238
  worktree_paths: dict[int, Path] = {} # index -> worktree path
235
239
  main_repo_paths: dict[int, Path] = {} # index -> main repo
240
+ worktree_warnings: list[str] = []
236
241
 
237
242
  # Get CLAUDE_TEAM_PROJECT_DIR for "auto" paths
238
243
  env_project_dir = os.environ.get("CLAUDE_TEAM_PROJECT_DIR")
@@ -240,9 +245,26 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
240
245
  for i, (w, name) in enumerate(zip(workers, resolved_names)):
241
246
  project_path = w["project_path"]
242
247
  use_worktree = w.get("use_worktree", True) # Default True
248
+ worktree_config = w.get("worktree")
249
+ worktree_explicitly_requested = worktree_config is not None
250
+ worktree_branch = None
251
+ worktree_base = None
243
252
  bead = w.get("bead")
244
253
  annotation = w.get("annotation")
245
254
 
255
+ if worktree_config is not None:
256
+ if isinstance(worktree_config, dict):
257
+ worktree_branch = worktree_config.get("branch")
258
+ worktree_base = worktree_config.get("base")
259
+ use_worktree = True
260
+ elif isinstance(worktree_config, bool):
261
+ use_worktree = worktree_config
262
+ else:
263
+ return error_response(
264
+ f"Worker {i} has invalid 'worktree' configuration",
265
+ hint="Expected a dict with optional 'branch'/'base' fields or a boolean.",
266
+ )
267
+
246
268
  # Step 1: Resolve repo path
247
269
  if project_path == "auto":
248
270
  if env_project_dir:
@@ -272,16 +294,28 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
272
294
  worker_name=name,
273
295
  bead_id=bead,
274
296
  annotation=annotation,
297
+ branch=worktree_branch,
298
+ base=worktree_base,
275
299
  )
276
300
  worktree_paths[i] = worktree_path
277
301
  main_repo_paths[i] = repo_path
278
302
  resolved_paths.append(str(worktree_path))
279
303
  logger.info(f"Created local worktree for {name} at {worktree_path}")
280
304
  except WorktreeError as e:
281
- logger.warning(
305
+ warning_message = (
282
306
  f"Failed to create worktree for {name}: {e}. "
283
307
  "Using repo directly."
284
308
  )
309
+ if worktree_explicitly_requested:
310
+ return error_response(
311
+ warning_message,
312
+ hint=(
313
+ "Verify the worktree branch/base settings and "
314
+ "that the repository is in a clean state."
315
+ ),
316
+ )
317
+ logger.warning(warning_message)
318
+ worktree_warnings.append(warning_message)
285
319
  resolved_paths.append(str(repo_path))
286
320
  else:
287
321
  # No worktree - use repo directly
@@ -290,44 +324,72 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
290
324
  # Pre-generate session IDs for Stop hook injection
291
325
  session_ids = [str(uuid.uuid4())[:8] for _ in workers]
292
326
 
293
- # Build profile customizations for each worker
294
- profile_customizations: list[LocalWriteOnlyProfile] = []
295
- for i, (w, name) in enumerate(zip(workers, resolved_names)):
296
- customization = LocalWriteOnlyProfile()
327
+ # Build profile customizations for each worker (iTerm-only)
328
+ profile_customizations: list[object | None] = [None] * worker_count
329
+ if isinstance(backend, ItermBackend):
330
+ from iterm2.profile import LocalWriteOnlyProfile
297
331
 
298
- bead = w.get("bead")
299
- annotation = w.get("annotation")
300
- agent_type = w.get("agent_type", "claude")
332
+ profile_customizations = []
333
+ for i, (w, name) in enumerate(zip(workers, resolved_names)):
334
+ customization = LocalWriteOnlyProfile()
301
335
 
302
- # Tab title
303
- tab_title = format_session_title(name, issue_id=bead, annotation=annotation)
304
- customization.set_name(tab_title)
336
+ bead = w.get("bead")
337
+ annotation = w.get("annotation")
338
+ agent_type = w.get("agent_type", "claude")
305
339
 
306
- # Tab color (unique per worker)
307
- color = generate_tab_color(base_index + i)
308
- customization.set_tab_color(color)
309
- customization.set_use_tab_color(True)
340
+ # Tab title
341
+ tab_title = format_session_title(
342
+ name, issue_id=bead, annotation=annotation
343
+ )
344
+ customization.set_name(tab_title)
310
345
 
311
- # Badge (multi-line with bead/name, annotation, and agent type indicator)
312
- badge_text = format_badge_text(
313
- name, bead=bead, annotation=annotation, agent_type=agent_type
314
- )
315
- customization.set_badge_text(badge_text)
346
+ # Tab color (unique per worker)
347
+ color = generate_tab_color(base_index + i)
348
+ customization.set_tab_color(color)
349
+ customization.set_use_tab_color(True)
350
+
351
+ # Badge (multi-line with bead/name, annotation, and agent type indicator)
352
+ badge_text = format_badge_text(
353
+ name, bead=bead, annotation=annotation, agent_type=agent_type
354
+ )
355
+ customization.set_badge_text(badge_text)
316
356
 
317
- # Apply current appearance mode colors
318
- await apply_appearance_colors(customization, connection)
357
+ # Apply current appearance mode colors
358
+ await apply_appearance_colors(customization, backend.connection)
319
359
 
320
- profile_customizations.append(customization)
360
+ profile_customizations.append(customization)
321
361
 
322
362
  # Create panes based on layout mode
323
- pane_sessions: list = [] # list of iTerm sessions
324
-
325
- if layout == "auto":
363
+ pane_sessions: list = [] # list of terminal handles
364
+
365
+ if backend.backend_id == "tmux":
366
+ for i in range(worker_count):
367
+ pane_sessions.append(
368
+ await backend.create_session(
369
+ name=resolved_names[i],
370
+ project_path=resolved_paths[i],
371
+ issue_id=workers[i].get("bead"),
372
+ coordinator_annotation=workers[i].get("annotation"),
373
+ )
374
+ )
375
+ elif layout == "auto":
326
376
  # Try to find an existing window where the ENTIRE batch fits.
327
377
  # This keeps spawn batches together rather than spreading across windows.
328
- target_tab = None
378
+ reuse_window = False
329
379
  initial_pane_count = 0
330
- first_session = None # Session to split from
380
+ first_session = None # Terminal handle to split from
381
+
382
+ def _count_tmux_panes(target_session, sessions) -> int:
383
+ session_name = target_session.metadata.get("session_name")
384
+ window_index = target_session.metadata.get("window_index")
385
+ if not session_name or window_index is None:
386
+ return 1
387
+ return sum(
388
+ 1
389
+ for session in sessions
390
+ if session.metadata.get("session_name") == session_name
391
+ and session.metadata.get("window_index") == window_index
392
+ )
331
393
 
332
394
  # Prefer the coordinator's window when running inside iTerm2.
333
395
  # ITERM_SESSION_ID format is "wXtYpZ:UUID" - extract just the UUID.
@@ -335,67 +397,65 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
335
397
  coordinator_session_id = None
336
398
  if iterm_session_env and ":" in iterm_session_env:
337
399
  coordinator_session_id = iterm_session_env.split(":", 1)[1]
338
- if coordinator_session_id:
339
- coordinator_session = None
340
- coordinator_tab = None
341
-
342
- for window in app.terminal_windows:
343
- for tab in window.tabs:
344
- for session in tab.sessions:
345
- if session.session_id == coordinator_session_id:
346
- coordinator_session = session
347
- coordinator_tab = tab
348
- break
349
- if coordinator_session:
350
- break
351
- if coordinator_session:
400
+ if coordinator_session_id and isinstance(backend, ItermBackend):
401
+ coordinator_handle = None
402
+ for session_handle in await backend.list_sessions():
403
+ if session_handle.native_id == coordinator_session_id:
404
+ coordinator_handle = session_handle
352
405
  break
353
406
 
354
- if coordinator_session and coordinator_tab:
355
- coordinator_window = await get_window_for_session(
356
- app, coordinator_session
407
+ if coordinator_handle:
408
+ coordinator_window = await backend.get_window_for_handle(
409
+ coordinator_handle
357
410
  )
358
411
  if coordinator_window is not None:
359
- initial_pane_count = len(coordinator_tab.sessions)
360
- available_slots = MAX_PANES_PER_TAB - initial_pane_count
361
- if worker_count <= available_slots:
362
- target_tab = coordinator_tab
363
- first_session = coordinator_session
364
- logger.debug(
365
- "Using coordinator window "
366
- f"({initial_pane_count} panes, {available_slots} slots)"
367
- )
368
-
369
- if target_tab is None:
370
- managed_iterm_ids: set[str] = {
371
- s.iterm_session.session_id
412
+ native_session = backend.unwrap_session(coordinator_handle)
413
+ coordinator_tab = native_session.tab
414
+ if coordinator_tab is not None:
415
+ initial_pane_count = len(coordinator_tab.sessions)
416
+ available_slots = MAX_PANES_PER_TAB - initial_pane_count
417
+ if worker_count <= available_slots:
418
+ reuse_window = True
419
+ first_session = coordinator_handle
420
+ logger.debug(
421
+ "Using coordinator window "
422
+ f"({initial_pane_count} panes, {available_slots} slots)"
423
+ )
424
+
425
+ if not reuse_window:
426
+ managed_session_ids = {
427
+ s.terminal_session.native_id
372
428
  for s in registry.list_all()
373
- if s.iterm_session is not None
429
+ if s.terminal_session.backend_id == backend.backend_id
374
430
  }
375
431
 
376
432
  # Find a window with enough space for ALL workers
377
- result = await find_available_window(
378
- app,
433
+ result = await backend.find_available_window(
379
434
  max_panes=MAX_PANES_PER_TAB,
380
- managed_session_ids=managed_iterm_ids,
435
+ managed_session_ids=managed_session_ids,
381
436
  )
382
437
 
383
438
  if result:
384
- window, tab, existing_session = result
385
- initial_pane_count = len(tab.sessions)
439
+ _, tab_or_window, existing_session = result
440
+ first_session = existing_session
441
+ if isinstance(backend, ItermBackend):
442
+ initial_pane_count = len(tab_or_window.sessions)
443
+ else:
444
+ initial_pane_count = _count_tmux_panes(
445
+ existing_session, await backend.list_sessions()
446
+ )
386
447
  available_slots = MAX_PANES_PER_TAB - initial_pane_count
387
448
 
388
449
  if worker_count <= available_slots:
389
450
  # Entire batch fits in this window
390
- target_tab = tab
391
- first_session = existing_session
451
+ reuse_window = True
392
452
  logger.debug(
393
453
  f"Batch of {worker_count} fits in existing window "
394
454
  f"({initial_pane_count} panes, {available_slots} slots)"
395
455
  )
396
456
 
397
- if target_tab:
398
- # Reuse existing window - track pane count locally (iTerm objects stale)
457
+ if reuse_window and first_session is not None:
458
+ # Reuse existing window - track pane count locally
399
459
  local_pane_count = initial_pane_count
400
460
  final_pane_count = initial_pane_count + worker_count
401
461
  # Track created sessions for splitting
@@ -411,7 +471,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
411
471
  # | worker2
412
472
  if local_pane_count == 1:
413
473
  # First split: vertical from coordinator
414
- new_session = await split_pane(
474
+ new_session = await backend.split_pane(
415
475
  first_session,
416
476
  vertical=True,
417
477
  before=False,
@@ -420,8 +480,11 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
420
480
  )
421
481
  else:
422
482
  # Second split: horizontal from first worker (stack on right)
423
- new_session = await split_pane(
424
- created_sessions[0],
483
+ split_target = (
484
+ created_sessions[0] if created_sessions else first_session
485
+ )
486
+ new_session = await backend.split_pane(
487
+ split_target,
425
488
  vertical=False,
426
489
  before=False,
427
490
  profile=None,
@@ -431,7 +494,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
431
494
  # Quad pattern: TL→TR(vsplit)→BL(hsplit)→BR(hsplit)
432
495
  if local_pane_count == 1:
433
496
  # First split: vertical (left/right)
434
- new_session = await split_pane(
497
+ new_session = await backend.split_pane(
435
498
  first_session,
436
499
  vertical=True,
437
500
  before=False,
@@ -440,7 +503,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
440
503
  )
441
504
  elif local_pane_count == 2:
442
505
  # Second split: horizontal from left pane (bottom-left)
443
- new_session = await split_pane(
506
+ new_session = await backend.split_pane(
444
507
  first_session,
445
508
  vertical=False,
446
509
  before=False,
@@ -452,7 +515,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
452
515
  tr_session = (
453
516
  created_sessions[0] if created_sessions else first_session
454
517
  )
455
- new_session = await split_pane(
518
+ new_session = await backend.split_pane(
456
519
  tr_session,
457
520
  vertical=False,
458
521
  before=False,
@@ -482,12 +545,14 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
482
545
  window_layout = "quad"
483
546
  pane_names = ["top_left", "top_right", "bottom_left", "bottom_right"]
484
547
 
485
- customizations_dict = {
486
- pane_names[i]: profile_customizations[i] for i in range(worker_count)
487
- }
548
+ customizations_dict = None
549
+ if isinstance(backend, ItermBackend):
550
+ customizations_dict = {
551
+ pane_names[i]: profile_customizations[i]
552
+ for i in range(worker_count)
553
+ }
488
554
 
489
- panes = await create_multi_pane_layout(
490
- connection,
555
+ panes = await backend.create_multi_pane_layout(
491
556
  window_layout,
492
557
  profile=None,
493
558
  profile_customizations=customizations_dict,
@@ -511,12 +576,14 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
511
576
  pane_names = ["top_left", "top_right", "bottom_left", "bottom_right"]
512
577
 
513
578
  # Build customizations dict for layout
514
- customizations_dict = {
515
- pane_names[i]: profile_customizations[i] for i in range(worker_count)
516
- }
579
+ customizations_dict = None
580
+ if isinstance(backend, ItermBackend):
581
+ customizations_dict = {
582
+ pane_names[i]: profile_customizations[i]
583
+ for i in range(worker_count)
584
+ }
517
585
 
518
- panes = await create_multi_pane_layout(
519
- connection,
586
+ panes = await backend.create_multi_pane_layout(
520
587
  window_layout,
521
588
  profile=None,
522
589
  profile_customizations=customizations_dict,
@@ -549,25 +616,16 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
549
616
  else:
550
617
  env = None
551
618
 
552
- if agent_type == "codex":
553
- # Start Codex in interactive mode using start_agent_in_session
554
- cli = get_cli_backend("codex")
555
- await start_agent_in_session(
556
- session=session,
557
- cli=cli,
558
- project_path=project_path,
559
- dangerously_skip_permissions=worker_config.get("skip_permissions", False),
560
- env=env,
561
- )
562
- else:
563
- # For Claude: use start_claude_in_session (convenience wrapper)
564
- await start_claude_in_session(
565
- session=session,
566
- project_path=project_path,
567
- dangerously_skip_permissions=worker_config.get("skip_permissions", False),
568
- env=env,
569
- stop_hook_marker_id=marker_id,
570
- )
619
+ cli = get_cli_backend(agent_type)
620
+ stop_hook_marker_id = marker_id if agent_type == "claude" else None
621
+ await backend.start_agent_in_session(
622
+ handle=session,
623
+ cli=cli,
624
+ project_path=project_path,
625
+ dangerously_skip_permissions=worker_config.get("skip_permissions", False),
626
+ env=env,
627
+ stop_hook_marker_id=stop_hook_marker_id,
628
+ )
571
629
 
572
630
  await asyncio.gather(*[start_agent_for_worker(i) for i in range(worker_count)])
573
631
 
@@ -575,7 +633,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
575
633
  managed_sessions = []
576
634
  for i in range(worker_count):
577
635
  managed = registry.add(
578
- iterm_session=pane_sessions[i],
636
+ terminal_session=pane_sessions[i],
579
637
  project_path=resolved_paths[i],
580
638
  name=resolved_names[i],
581
639
  session_id=session_ids[i],
@@ -590,15 +648,28 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
590
648
  managed.main_repo_path = main_repo_paths[i]
591
649
  managed_sessions.append(managed)
592
650
 
593
- # Send marker messages for JSONL correlation (Claude only)
594
- # Codex doesn't use JSONL markers for session tracking
651
+ # Send marker messages for JSONL correlation (Claude + Codex)
595
652
  for i, managed in enumerate(managed_sessions):
596
- if managed.agent_type == "claude":
597
- marker_message = generate_marker_message(
598
- managed.session_id,
599
- iterm_session_id=managed.iterm_session.session_id,
600
- )
601
- await send_prompt(pane_sessions[i], marker_message, submit=True)
653
+ iterm_session_id = None
654
+ tmux_pane_ids = None
655
+ if managed.terminal_session.backend_id == "iterm":
656
+ iterm_session_id = managed.terminal_session.native_id
657
+ elif managed.terminal_session.backend_id == "tmux":
658
+ tmux_pane_ids = [managed.terminal_session.native_id]
659
+ marker_message = generate_marker_message(
660
+ managed.session_id,
661
+ iterm_session_id=iterm_session_id,
662
+ tmux_pane_ids=tmux_pane_ids,
663
+ project_path=(
664
+ managed.project_path if managed.agent_type == "codex" else None
665
+ ),
666
+ )
667
+ await backend.send_prompt_for_agent(
668
+ pane_sessions[i],
669
+ marker_message,
670
+ agent_type=managed.agent_type,
671
+ submit=True,
672
+ )
602
673
 
603
674
  # Wait for markers to appear in JSONL (Claude only)
604
675
  for i, managed in enumerate(managed_sessions):
@@ -646,8 +717,17 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
646
717
 
647
718
  # Send prompt to the already-running agent (both Claude and Codex)
648
719
  # Use agent-specific timing (Codex needs longer delay before Enter)
649
- logger.info(f"Sending prompt to {managed.name} (agent_type={managed.agent_type}, chars={len(worker_prompt)})")
650
- await send_prompt_for_agent(pane_sessions[i], worker_prompt, agent_type=managed.agent_type)
720
+ logger.info(
721
+ "Sending prompt to %s (agent_type=%s, chars=%d)",
722
+ managed.name,
723
+ managed.agent_type,
724
+ len(worker_prompt),
725
+ )
726
+ await backend.send_prompt_for_agent(
727
+ pane_sessions[i],
728
+ worker_prompt,
729
+ agent_type=managed.agent_type,
730
+ )
651
731
  logger.info(f"Prompt sent to {managed.name}")
652
732
 
653
733
  # Mark sessions ready
@@ -657,16 +737,13 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
657
737
  result_sessions[managed.name] = managed.to_dict()
658
738
 
659
739
  # Re-activate the window to bring it to focus
660
- try:
661
- await app.async_activate()
662
- if pane_sessions:
663
- tab = pane_sessions[0].tab
664
- if tab is not None:
665
- window = tab.window
666
- if window is not None:
667
- await window.async_activate()
668
- except Exception as e:
669
- logger.debug(f"Failed to re-activate window: {e}")
740
+ if isinstance(backend, ItermBackend):
741
+ try:
742
+ await backend.activate_app()
743
+ if pane_sessions:
744
+ await backend.activate_window_for_handle(pane_sessions[0])
745
+ except Exception as e:
746
+ logger.debug(f"Failed to re-activate window: {e}")
670
747
 
671
748
  # Build worker summaries for coordinator guidance
672
749
  worker_summaries = []
@@ -695,6 +772,8 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
695
772
  # Add structured warning for programmatic access
696
773
  if workers_awaiting_task:
697
774
  result["workers_awaiting_task"] = workers_awaiting_task
775
+ if worktree_warnings:
776
+ result["warnings"] = worktree_warnings
698
777
 
699
778
  return result
700
779
 
@@ -92,6 +92,7 @@ def register_tools(mcp: FastMCP) -> None:
92
92
  session_infos.append(SessionInfo(
93
93
  jsonl_path=jsonl_path,
94
94
  session_id=session.session_id, # Must use internal ID to match stop hook marker
95
+ agent_type=session.agent_type,
95
96
  ))
96
97
 
97
98
  # Report any missing sessions/files
@@ -34,7 +34,7 @@ def error_response(
34
34
  HINTS = {
35
35
  "session_not_found": (
36
36
  "Run list_workers to see available workers, or discover_workers "
37
- "to find orphaned iTerm2 sessions that can be adopted"
37
+ "to find orphaned terminal sessions that can be adopted"
38
38
  ),
39
39
  "project_path_missing": (
40
40
  "Verify the path exists. For git worktrees, check 'git worktree list'. "
@@ -44,9 +44,13 @@ HINTS = {
44
44
  "Ensure iTerm2 is running and Python API is enabled: "
45
45
  "iTerm2 → Preferences → General → Magic → Enable Python API"
46
46
  ),
47
+ "terminal_backend_required": (
48
+ "This tool only supports the iTerm2 or tmux backends. "
49
+ "Set CLAUDE_TEAM_TERMINAL_BACKEND=iterm or tmux, or run inside a supported terminal."
50
+ ),
47
51
  "registry_empty": (
48
52
  "No workers are being managed. Use spawn_workers to create new workers, "
49
- "or discover_workers to find existing Claude sessions in iTerm2"
53
+ "or discover_workers to find existing Claude sessions in a supported terminal"
50
54
  ),
51
55
  "no_jsonl_file": (
52
56
  "Claude may not have started yet or the session file doesn't exist. "
@@ -73,7 +77,7 @@ def get_session_or_error(
73
77
 
74
78
  Args:
75
79
  registry: The session registry to search
76
- session_id: ID to resolve (supports session_id, iterm_session_id, or name)
80
+ session_id: ID to resolve (supports session_id, terminal_id, or name)
77
81
 
78
82
  Returns:
79
83
  ManagedSession if found, or error dict with hint if not found