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.
- claude_team/__init__.py +11 -0
- claude_team/events.py +477 -0
- claude_team/idle_detection.py +173 -0
- claude_team/poller.py +245 -0
- claude_team_mcp/idle_detection.py +16 -3
- claude_team_mcp/iterm_utils.py +5 -73
- claude_team_mcp/registry.py +43 -26
- claude_team_mcp/server.py +95 -61
- claude_team_mcp/session_state.py +364 -2
- claude_team_mcp/terminal_backends/__init__.py +31 -0
- claude_team_mcp/terminal_backends/base.py +106 -0
- claude_team_mcp/terminal_backends/iterm.py +251 -0
- claude_team_mcp/terminal_backends/tmux.py +646 -0
- claude_team_mcp/tools/__init__.py +4 -2
- claude_team_mcp/tools/adopt_worker.py +89 -32
- claude_team_mcp/tools/close_workers.py +39 -10
- claude_team_mcp/tools/discover_workers.py +176 -32
- claude_team_mcp/tools/list_workers.py +29 -0
- claude_team_mcp/tools/message_workers.py +35 -5
- claude_team_mcp/tools/poll_worker_changes.py +227 -0
- claude_team_mcp/tools/spawn_workers.py +221 -142
- claude_team_mcp/tools/wait_idle_workers.py +1 -0
- claude_team_mcp/utils/errors.py +7 -3
- claude_team_mcp/worktree.py +59 -12
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.7.0.dist-info}/METADATA +1 -1
- claude_team_mcp-0.7.0.dist-info/RECORD +52 -0
- claude_team_mcp-0.6.1.dist-info/RECORD +0 -43
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {claude_team_mcp-0.6.1.dist-info → claude_team_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
69
|
-
and optional worktree. Workers can be
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
332
|
+
profile_customizations = []
|
|
333
|
+
for i, (w, name) in enumerate(zip(workers, resolved_names)):
|
|
334
|
+
customization = LocalWriteOnlyProfile()
|
|
301
335
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
336
|
+
bead = w.get("bead")
|
|
337
|
+
annotation = w.get("annotation")
|
|
338
|
+
agent_type = w.get("agent_type", "claude")
|
|
305
339
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
|
|
357
|
+
# Apply current appearance mode colors
|
|
358
|
+
await apply_appearance_colors(customization, backend.connection)
|
|
319
359
|
|
|
320
|
-
|
|
360
|
+
profile_customizations.append(customization)
|
|
321
361
|
|
|
322
362
|
# Create panes based on layout mode
|
|
323
|
-
pane_sessions: list = [] # list of
|
|
324
|
-
|
|
325
|
-
if
|
|
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
|
-
|
|
378
|
+
reuse_window = False
|
|
329
379
|
initial_pane_count = 0
|
|
330
|
-
first_session = None #
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
355
|
-
coordinator_window = await
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
if
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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.
|
|
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=
|
|
435
|
+
managed_session_ids=managed_session_ids,
|
|
381
436
|
)
|
|
382
437
|
|
|
383
438
|
if result:
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
|
398
|
-
# Reuse existing window - track pane count locally
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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(
|
|
650
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
claude_team_mcp/utils/errors.py
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|