claude-team-mcp 0.4.0__py3-none-any.whl → 0.6.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.
@@ -58,10 +58,9 @@ class CodexCLI(AgentCLI):
58
58
  """
59
59
  args: list[str] = []
60
60
 
61
- # Codex uses --dangerously-bypass-approvals-and-sandbox for autonomous operation
62
- # (--full-auto doesn't work through happy wrapper)
61
+ # Codex uses --full-auto for autonomous operation.
63
62
  if dangerously_skip_permissions:
64
- args.append("--dangerously-bypass-approvals-and-sandbox")
63
+ args.append("--full-auto")
65
64
 
66
65
  # Note: settings_file is ignored - Codex doesn't support this
67
66
  # Idle detection uses session file polling instead
@@ -0,0 +1,132 @@
1
+ """
2
+ Issue tracker abstraction module.
3
+
4
+ Defines a protocol and backend implementations for issue tracker commands.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from typing import Protocol, runtime_checkable
13
+
14
+ logger = logging.getLogger("claude-team-mcp")
15
+
16
+
17
+ @runtime_checkable
18
+ class IssueTrackerBackend(Protocol):
19
+ """
20
+ Protocol defining the issue tracker backend interface.
21
+
22
+ Backends provide a name, CLI command, marker directory, and command templates.
23
+ """
24
+
25
+ name: str
26
+ cli: str
27
+ marker_dir: str
28
+ commands: dict[str, str]
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class BeadsBackend:
33
+ """Beads issue tracker backend."""
34
+
35
+ name: str = "beads"
36
+ cli: str = "bd"
37
+ marker_dir: str = ".beads"
38
+ commands: dict[str, str] = field(
39
+ default_factory=lambda: {
40
+ "list": "bd --no-db list",
41
+ "ready": "bd --no-db ready",
42
+ "show": "bd --no-db show {issue_id}",
43
+ "update": "bd --no-db update {issue_id} --status {status}",
44
+ "close": "bd --no-db close {issue_id}",
45
+ "create": (
46
+ "bd --no-db create --title \"{title}\" --type {type} "
47
+ "--priority {priority} --description \"{description}\""
48
+ ),
49
+ "comment": "bd --no-db comment {issue_id} \"{comment}\"",
50
+ "dep_add": "bd --no-db dep add {issue_id} {dependency_id}",
51
+ "dep_tree": "bd --no-db dep tree {issue_id}",
52
+ }
53
+ )
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class PebblesBackend:
58
+ """Pebbles issue tracker backend."""
59
+
60
+ name: str = "pebbles"
61
+ cli: str = "pb"
62
+ marker_dir: str = ".pebbles"
63
+ commands: dict[str, str] = field(
64
+ default_factory=lambda: {
65
+ "list": "pb list",
66
+ "ready": "pb ready",
67
+ "show": "pb show {issue_id}",
68
+ "update": "pb update {issue_id} -status {status}",
69
+ "close": "pb close {issue_id}",
70
+ "create": (
71
+ "pb create -title \"{title}\" -type {type} -priority {priority} "
72
+ "-description \"{description}\""
73
+ ),
74
+ "comment": "pb comment {issue_id} -body \"{comment}\"",
75
+ "dep_add": "pb dep add {issue_id} {dependency_id}",
76
+ "dep_tree": "pb dep tree {issue_id}",
77
+ }
78
+ )
79
+
80
+
81
+ BEADS_BACKEND = BeadsBackend()
82
+ PEBBLES_BACKEND = PebblesBackend()
83
+ BACKEND_REGISTRY: dict[str, IssueTrackerBackend] = {
84
+ BEADS_BACKEND.name: BEADS_BACKEND,
85
+ PEBBLES_BACKEND.name: PEBBLES_BACKEND,
86
+ }
87
+
88
+
89
+ def detect_issue_tracker(project_path: str) -> IssueTrackerBackend | None:
90
+ """
91
+ Detect the issue tracker backend for the given project path.
92
+
93
+ Args:
94
+ project_path: Absolute or relative path to the project root.
95
+
96
+ Returns:
97
+ The detected IssueTrackerBackend, or None if no markers are present.
98
+ """
99
+ beads_marker = os.path.join(project_path, BEADS_BACKEND.marker_dir)
100
+ pebbles_marker = os.path.join(project_path, PEBBLES_BACKEND.marker_dir)
101
+
102
+ # Check marker directories in the project root.
103
+ beads_present = os.path.isdir(beads_marker)
104
+ pebbles_present = os.path.isdir(pebbles_marker)
105
+
106
+ # Resolve the deterministic backend when both markers exist.
107
+ if beads_present and pebbles_present:
108
+ logger.warning(
109
+ "Both .beads and .pebbles found in %s; defaulting to pebbles",
110
+ project_path,
111
+ )
112
+ return PEBBLES_BACKEND
113
+
114
+ # Return the matching backend if only one marker exists.
115
+ if pebbles_present:
116
+ return PEBBLES_BACKEND
117
+
118
+ if beads_present:
119
+ return BEADS_BACKEND
120
+
121
+ return None
122
+
123
+
124
+ __all__ = [
125
+ "IssueTrackerBackend",
126
+ "BeadsBackend",
127
+ "PebblesBackend",
128
+ "BEADS_BACKEND",
129
+ "PEBBLES_BACKEND",
130
+ "BACKEND_REGISTRY",
131
+ "detect_issue_tracker",
132
+ ]
@@ -8,7 +8,7 @@ from mcp.server.fastmcp import FastMCP
8
8
 
9
9
  from . import adopt_worker
10
10
  from . import annotate_worker
11
- from . import bd_help
11
+ from . import issue_tracker_help
12
12
  from . import check_idle_workers
13
13
  from . import close_workers
14
14
  from . import discover_workers
@@ -31,7 +31,7 @@ def register_all_tools(mcp: FastMCP, ensure_connection) -> None:
31
31
  """
32
32
  # Tools that don't need ensure_connection
33
33
  annotate_worker.register_tools(mcp)
34
- bd_help.register_tools(mcp)
34
+ issue_tracker_help.register_tools(mcp)
35
35
  check_idle_workers.register_tools(mcp)
36
36
  close_workers.register_tools(mcp)
37
37
  examine_worker.register_tools(mcp)
@@ -0,0 +1,50 @@
1
+ """
2
+ Issue tracker help tool.
3
+
4
+ Provides issue_tracker_help for quick reference on issue tracking commands.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from ..issue_tracker import BACKEND_REGISTRY, detect_issue_tracker
12
+ from ..utils import build_issue_tracker_help_text, build_issue_tracker_quick_commands
13
+
14
+
15
+ def register_tools(mcp: FastMCP) -> None:
16
+ """Register issue_tracker_help tool on the MCP server."""
17
+
18
+ @mcp.tool()
19
+ async def issue_tracker_help() -> dict:
20
+ """
21
+ Get a quick reference guide for using issue tracking.
22
+
23
+ Returns condensed documentation on tracker commands, workflow patterns,
24
+ and best practices for worker sessions. Call this tool when you need
25
+ guidance on tracking progress, adding comments, or managing issues.
26
+
27
+ Returns:
28
+ Dict with help text and key command examples
29
+ """
30
+ project_path = str(Path.cwd())
31
+ backend = detect_issue_tracker(project_path)
32
+ help_text = build_issue_tracker_help_text(backend)
33
+ quick_commands = build_issue_tracker_quick_commands(backend)
34
+
35
+ response = {
36
+ "help": help_text,
37
+ "quick_commands": quick_commands,
38
+ "worker_tip": (
39
+ "As a worker, add comments to track progress rather than closing issues. "
40
+ "The coordinator will close issues after reviewing your work."
41
+ ),
42
+ }
43
+
44
+ if backend is None:
45
+ response["supported_trackers"] = sorted(BACKEND_REGISTRY.keys())
46
+ else:
47
+ response["tracker"] = backend.name
48
+ response["cli"] = backend.cli
49
+
50
+ return response
@@ -19,9 +19,10 @@ from ..idle_detection import (
19
19
  wait_for_any_idle as wait_for_any_idle_impl,
20
20
  SessionInfo,
21
21
  )
22
+ from ..issue_tracker import detect_issue_tracker
22
23
  from ..iterm_utils import send_prompt_for_agent
23
24
  from ..registry import SessionStatus
24
- from ..utils import error_response, HINTS, WORKER_MESSAGE_HINT
25
+ from ..utils import build_worker_message_hint, error_response, HINTS
25
26
 
26
27
  logger = logging.getLogger("claude-team-mcp")
27
28
 
@@ -187,8 +188,14 @@ def register_tools(mcp: FastMCP) -> None:
187
188
  # Update status to busy
188
189
  registry.update_status(sid, SessionStatus.BUSY)
189
190
 
190
- # Append hint about bd_help tool to help workers understand beads
191
- message_with_hint = message + WORKER_MESSAGE_HINT
191
+ # Append tracker-specific hint so workers know how to log progress.
192
+ tracker_path = (
193
+ str(session.main_repo_path)
194
+ if session.main_repo_path is not None
195
+ else session.project_path
196
+ )
197
+ tracker_backend = detect_issue_tracker(tracker_path)
198
+ message_with_hint = message + build_worker_message_hint(tracker_backend)
192
199
 
193
200
  # Send the message using agent-specific input handling.
194
201
  # Codex needs a longer pre-Enter delay than Claude.
@@ -23,6 +23,7 @@ from ..iterm_utils import (
23
23
  MAX_PANES_PER_TAB,
24
24
  create_multi_pane_layout,
25
25
  find_available_window,
26
+ get_window_for_session,
26
27
  send_prompt,
27
28
  send_prompt_for_agent,
28
29
  split_pane,
@@ -32,7 +33,7 @@ from ..iterm_utils import (
32
33
  from ..names import pick_names_for_count
33
34
  from ..profile import apply_appearance_colors
34
35
  from ..registry import SessionStatus
35
- from ..utils import HINTS, error_response, get_worktree_beads_dir
36
+ from ..utils import HINTS, error_response, get_worktree_tracker_dir
36
37
  from ..worker_prompt import generate_worker_prompt, get_coordinator_guidance
37
38
  from ..worktree import WorktreeError, create_local_worktree
38
39
 
@@ -324,76 +325,140 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
324
325
  if layout == "auto":
325
326
  # Try to find an existing window where the ENTIRE batch fits.
326
327
  # This keeps spawn batches together rather than spreading across windows.
327
- managed_iterm_ids: set[str] = {
328
- s.iterm_session.session_id
329
- for s in registry.list_all()
330
- if s.iterm_session is not None
331
- }
332
-
333
- # Find a window with enough space for ALL workers
334
- result = await find_available_window(
335
- app,
336
- max_panes=MAX_PANES_PER_TAB,
337
- managed_session_ids=managed_iterm_ids,
338
- )
339
-
340
328
  target_tab = None
341
329
  initial_pane_count = 0
342
330
  first_session = None # Session to split from
343
331
 
344
- if result:
345
- window, tab, existing_session = result
346
- initial_pane_count = len(tab.sessions)
347
- available_slots = MAX_PANES_PER_TAB - initial_pane_count
348
-
349
- if worker_count <= available_slots:
350
- # Entire batch fits in this window
351
- target_tab = tab
352
- first_session = existing_session
353
- logger.debug(
354
- f"Batch of {worker_count} fits in existing window "
355
- f"({initial_pane_count} panes, {available_slots} slots)"
332
+ # Prefer the coordinator's window when running inside iTerm2.
333
+ # ITERM_SESSION_ID format is "wXtYpZ:UUID" - extract just the UUID.
334
+ iterm_session_env = os.environ.get("ITERM_SESSION_ID")
335
+ coordinator_session_id = None
336
+ if iterm_session_env and ":" in iterm_session_env:
337
+ 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:
352
+ break
353
+
354
+ if coordinator_session and coordinator_tab:
355
+ coordinator_window = await get_window_for_session(
356
+ app, coordinator_session
356
357
  )
358
+ 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
372
+ for s in registry.list_all()
373
+ if s.iterm_session is not None
374
+ }
375
+
376
+ # Find a window with enough space for ALL workers
377
+ result = await find_available_window(
378
+ app,
379
+ max_panes=MAX_PANES_PER_TAB,
380
+ managed_session_ids=managed_iterm_ids,
381
+ )
382
+
383
+ if result:
384
+ window, tab, existing_session = result
385
+ initial_pane_count = len(tab.sessions)
386
+ available_slots = MAX_PANES_PER_TAB - initial_pane_count
387
+
388
+ if worker_count <= available_slots:
389
+ # Entire batch fits in this window
390
+ target_tab = tab
391
+ first_session = existing_session
392
+ logger.debug(
393
+ f"Batch of {worker_count} fits in existing window "
394
+ f"({initial_pane_count} panes, {available_slots} slots)"
395
+ )
357
396
 
358
397
  if target_tab:
359
398
  # Reuse existing window - track pane count locally (iTerm objects stale)
360
399
  local_pane_count = initial_pane_count
361
- # Track created sessions for quad splitting
400
+ final_pane_count = initial_pane_count + worker_count
401
+ # Track created sessions for splitting
362
402
  created_sessions: list = []
363
403
 
364
404
  for i in range(worker_count):
365
- # Determine split direction based on local_pane_count
366
- # Incremental quad: TL→TR(vsplit)→BL(hsplit)→BR(hsplit)
367
- if local_pane_count == 1:
368
- # First split: vertical (left/right)
369
- new_session = await split_pane(
370
- first_session,
371
- vertical=True,
372
- before=False,
373
- profile=None,
374
- profile_customizations=profile_customizations[i],
375
- )
376
- elif local_pane_count == 2:
377
- # Second split: horizontal from left pane (creates bottom-left)
378
- # Use first_session (the original TL pane)
379
- new_session = await split_pane(
380
- first_session,
381
- vertical=False,
382
- before=False,
383
- profile=None,
384
- profile_customizations=profile_customizations[i],
385
- )
386
- else: # local_pane_count == 3
387
- # Third split: horizontal from right pane (creates bottom-right)
388
- # The TR pane is the first one we created
389
- tr_session = created_sessions[0] if created_sessions else first_session
390
- new_session = await split_pane(
391
- tr_session,
392
- vertical=False,
393
- before=False,
394
- profile=None,
395
- profile_customizations=profile_customizations[i],
396
- )
405
+ # Choose layout strategy based on final pane count:
406
+ # - 3 panes: coordinator full left, workers stacked right
407
+ # - 4 panes: quad (TL→TR→BL→BR)
408
+ if final_pane_count == 3:
409
+ # Layout: coordinator | worker1
410
+ # |--------
411
+ # | worker2
412
+ if local_pane_count == 1:
413
+ # First split: vertical from coordinator
414
+ new_session = await split_pane(
415
+ first_session,
416
+ vertical=True,
417
+ before=False,
418
+ profile=None,
419
+ profile_customizations=profile_customizations[i],
420
+ )
421
+ else:
422
+ # Second split: horizontal from first worker (stack on right)
423
+ new_session = await split_pane(
424
+ created_sessions[0],
425
+ vertical=False,
426
+ before=False,
427
+ profile=None,
428
+ profile_customizations=profile_customizations[i],
429
+ )
430
+ else:
431
+ # Quad pattern: TL→TR(vsplit)→BL(hsplit)→BR(hsplit)
432
+ if local_pane_count == 1:
433
+ # First split: vertical (left/right)
434
+ new_session = await split_pane(
435
+ first_session,
436
+ vertical=True,
437
+ before=False,
438
+ profile=None,
439
+ profile_customizations=profile_customizations[i],
440
+ )
441
+ elif local_pane_count == 2:
442
+ # Second split: horizontal from left pane (bottom-left)
443
+ new_session = await split_pane(
444
+ first_session,
445
+ vertical=False,
446
+ before=False,
447
+ profile=None,
448
+ profile_customizations=profile_customizations[i],
449
+ )
450
+ else: # local_pane_count == 3
451
+ # Third split: horizontal from right pane (bottom-right)
452
+ tr_session = (
453
+ created_sessions[0] if created_sessions else first_session
454
+ )
455
+ new_session = await split_pane(
456
+ tr_session,
457
+ vertical=False,
458
+ before=False,
459
+ profile=None,
460
+ profile_customizations=profile_customizations[i],
461
+ )
397
462
 
398
463
  pane_sessions.append(new_session)
399
464
  created_sessions.append(new_session)
@@ -476,9 +541,13 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
476
541
  marker_id = session_ids[index]
477
542
  agent_type = agent_types[index]
478
543
 
479
- # Check for worktree and set BEADS_DIR if needed
480
- beads_dir = get_worktree_beads_dir(project_path)
481
- env = {"BEADS_DIR": beads_dir} if beads_dir else None
544
+ # Check for worktree and set tracker env var if needed.
545
+ tracker_info = get_worktree_tracker_dir(project_path)
546
+ if tracker_info:
547
+ env_var, tracker_dir = tracker_info
548
+ env = {env_var: tracker_dir}
549
+ else:
550
+ env = None
482
551
 
483
552
  if agent_type == "codex":
484
553
  # Start Codex in interactive mode using start_agent_in_session
@@ -560,12 +629,18 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
560
629
  if not bead and not custom_prompt:
561
630
  workers_awaiting_task.append(managed.name)
562
631
 
632
+ tracker_path = (
633
+ str(managed.main_repo_path)
634
+ if managed.main_repo_path is not None
635
+ else managed.project_path
636
+ )
563
637
  worker_prompt = generate_worker_prompt(
564
638
  managed.session_id,
565
639
  resolved_names[i],
566
640
  agent_type=managed.agent_type,
567
641
  use_worktree=use_worktree,
568
642
  bead=bead,
643
+ project_path=tracker_path,
569
644
  custom_prompt=custom_prompt,
570
645
  )
571
646
 
@@ -2,16 +2,24 @@
2
2
  Shared utilities for Claude Team MCP tools.
3
3
  """
4
4
 
5
- from .constants import BEADS_HELP_TEXT, CONVERSATION_PAGE_SIZE, WORKER_MESSAGE_HINT
5
+ from .constants import (
6
+ CONVERSATION_PAGE_SIZE,
7
+ ISSUE_TRACKER_HELP_TOOL,
8
+ build_issue_tracker_help_text,
9
+ build_issue_tracker_quick_commands,
10
+ build_worker_message_hint,
11
+ )
6
12
  from .errors import error_response, HINTS, get_session_or_error
7
- from .worktree_detection import get_worktree_beads_dir
13
+ from .worktree_detection import get_worktree_tracker_dir
8
14
 
9
15
  __all__ = [
10
- "BEADS_HELP_TEXT",
11
16
  "CONVERSATION_PAGE_SIZE",
12
- "WORKER_MESSAGE_HINT",
17
+ "ISSUE_TRACKER_HELP_TOOL",
18
+ "build_issue_tracker_help_text",
19
+ "build_issue_tracker_quick_commands",
20
+ "build_worker_message_hint",
13
21
  "error_response",
14
22
  "HINTS",
15
23
  "get_session_or_error",
16
- "get_worktree_beads_dir",
24
+ "get_worktree_tracker_dir",
17
25
  ]