claude-team-mcp 0.3.2__py3-none-any.whl → 0.5.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_mcp/cli_backends/__init__.py +44 -0
- claude_team_mcp/cli_backends/base.py +132 -0
- claude_team_mcp/cli_backends/claude.py +110 -0
- claude_team_mcp/cli_backends/codex.py +110 -0
- claude_team_mcp/formatting.py +16 -1
- claude_team_mcp/idle_detection.py +289 -9
- claude_team_mcp/iterm_utils.py +256 -62
- claude_team_mcp/names.py +3 -0
- claude_team_mcp/registry.py +58 -20
- claude_team_mcp/schemas/__init__.py +5 -0
- claude_team_mcp/schemas/codex.py +267 -0
- claude_team_mcp/server.py +8 -1
- claude_team_mcp/session_state.py +214 -0
- claude_team_mcp/tools/message_workers.py +110 -16
- claude_team_mcp/tools/spawn_workers.py +81 -48
- claude_team_mcp/utils/constants.py +6 -0
- claude_team_mcp/worker_prompt.py +201 -15
- claude_team_mcp/worktree.py +29 -20
- {claude_team_mcp-0.3.2.dist-info → claude_team_mcp-0.5.0.dist-info}/METADATA +27 -3
- {claude_team_mcp-0.3.2.dist-info → claude_team_mcp-0.5.0.dist-info}/RECORD +22 -16
- {claude_team_mcp-0.3.2.dist-info → claude_team_mcp-0.5.0.dist-info}/WHEEL +0 -0
- {claude_team_mcp-0.3.2.dist-info → claude_team_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -8,7 +8,7 @@ import logging
|
|
|
8
8
|
import os
|
|
9
9
|
import uuid
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import TYPE_CHECKING, Literal,
|
|
11
|
+
from typing import TYPE_CHECKING, Literal, Required, TypedDict
|
|
12
12
|
|
|
13
13
|
from mcp.server.fastmcp import Context, FastMCP
|
|
14
14
|
from mcp.server.session import ServerSession
|
|
@@ -16,25 +16,23 @@ from mcp.server.session import ServerSession
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from ..server import AppContext
|
|
18
18
|
|
|
19
|
+
from ..cli_backends import get_cli_backend
|
|
19
20
|
from ..colors import generate_tab_color
|
|
20
21
|
from ..formatting import format_badge_text, format_session_title
|
|
21
22
|
from ..iterm_utils import (
|
|
22
|
-
LAYOUT_PANE_NAMES,
|
|
23
23
|
MAX_PANES_PER_TAB,
|
|
24
24
|
create_multi_pane_layout,
|
|
25
25
|
find_available_window,
|
|
26
26
|
send_prompt,
|
|
27
|
+
send_prompt_for_agent,
|
|
27
28
|
split_pane,
|
|
29
|
+
start_agent_in_session,
|
|
28
30
|
start_claude_in_session,
|
|
29
31
|
)
|
|
30
32
|
from ..names import pick_names_for_count
|
|
31
|
-
from ..profile import
|
|
32
|
-
PROFILE_NAME,
|
|
33
|
-
apply_appearance_colors,
|
|
34
|
-
get_or_create_profile,
|
|
35
|
-
)
|
|
33
|
+
from ..profile import apply_appearance_colors
|
|
36
34
|
from ..registry import SessionStatus
|
|
37
|
-
from ..utils import
|
|
35
|
+
from ..utils import HINTS, error_response, get_worktree_beads_dir
|
|
38
36
|
from ..worker_prompt import generate_worker_prompt, get_coordinator_guidance
|
|
39
37
|
from ..worktree import WorktreeError, create_local_worktree
|
|
40
38
|
|
|
@@ -45,6 +43,7 @@ class WorkerConfig(TypedDict, total=False):
|
|
|
45
43
|
"""Configuration for a single worker."""
|
|
46
44
|
|
|
47
45
|
project_path: Required[str] # Required: Path to repo, or "auto" to use env var
|
|
46
|
+
agent_type: str # Optional: "claude" (default) or "codex"
|
|
48
47
|
name: str # Optional: Worker name override. None = auto-pick from themed sets.
|
|
49
48
|
annotation: str # Optional: Task description (badge, branch, worker annotation)
|
|
50
49
|
bead: str # Optional: Beads issue ID (for badge, branch naming)
|
|
@@ -100,6 +99,9 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
```
|
|
102
|
+
agent_type: Which agent CLI to use (default "claude").
|
|
103
|
+
- "claude": Claude Code CLI (Stop hook idle detection)
|
|
104
|
+
- "codex": OpenAI Codex CLI (JSONL streaming idle detection)
|
|
103
105
|
use_worktree: Whether to create an isolated worktree (default True).
|
|
104
106
|
- True: Creates worktree at <repo>/.worktrees/<bead>-<annotation>
|
|
105
107
|
or <repo>/.worktrees/<name>-<uuid>-<annotation>
|
|
@@ -202,9 +204,6 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
202
204
|
connection, app = await ensure_connection(app_ctx)
|
|
203
205
|
|
|
204
206
|
try:
|
|
205
|
-
# Ensure the claude-team profile exists
|
|
206
|
-
await get_or_create_profile(connection)
|
|
207
|
-
|
|
208
207
|
# Get base session index for color generation
|
|
209
208
|
base_index = registry.count()
|
|
210
209
|
|
|
@@ -297,6 +296,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
297
296
|
|
|
298
297
|
bead = w.get("bead")
|
|
299
298
|
annotation = w.get("annotation")
|
|
299
|
+
agent_type = w.get("agent_type", "claude")
|
|
300
300
|
|
|
301
301
|
# Tab title
|
|
302
302
|
tab_title = format_session_title(name, issue_id=bead, annotation=annotation)
|
|
@@ -307,8 +307,10 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
307
307
|
customization.set_tab_color(color)
|
|
308
308
|
customization.set_use_tab_color(True)
|
|
309
309
|
|
|
310
|
-
# Badge (multi-line with bead/name and
|
|
311
|
-
badge_text = format_badge_text(
|
|
310
|
+
# Badge (multi-line with bead/name, annotation, and agent type indicator)
|
|
311
|
+
badge_text = format_badge_text(
|
|
312
|
+
name, bead=bead, annotation=annotation, agent_type=agent_type
|
|
313
|
+
)
|
|
312
314
|
customization.set_badge_text(badge_text)
|
|
313
315
|
|
|
314
316
|
# Apply current appearance mode colors
|
|
@@ -368,7 +370,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
368
370
|
first_session,
|
|
369
371
|
vertical=True,
|
|
370
372
|
before=False,
|
|
371
|
-
profile=
|
|
373
|
+
profile=None,
|
|
372
374
|
profile_customizations=profile_customizations[i],
|
|
373
375
|
)
|
|
374
376
|
elif local_pane_count == 2:
|
|
@@ -378,7 +380,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
378
380
|
first_session,
|
|
379
381
|
vertical=False,
|
|
380
382
|
before=False,
|
|
381
|
-
profile=
|
|
383
|
+
profile=None,
|
|
382
384
|
profile_customizations=profile_customizations[i],
|
|
383
385
|
)
|
|
384
386
|
else: # local_pane_count == 3
|
|
@@ -389,7 +391,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
389
391
|
tr_session,
|
|
390
392
|
vertical=False,
|
|
391
393
|
before=False,
|
|
392
|
-
profile=
|
|
394
|
+
profile=None,
|
|
393
395
|
profile_customizations=profile_customizations[i],
|
|
394
396
|
)
|
|
395
397
|
|
|
@@ -422,7 +424,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
422
424
|
panes = await create_multi_pane_layout(
|
|
423
425
|
connection,
|
|
424
426
|
window_layout,
|
|
425
|
-
profile=
|
|
427
|
+
profile=None,
|
|
426
428
|
profile_customizations=customizations_dict,
|
|
427
429
|
)
|
|
428
430
|
|
|
@@ -451,34 +453,54 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
451
453
|
panes = await create_multi_pane_layout(
|
|
452
454
|
connection,
|
|
453
455
|
window_layout,
|
|
454
|
-
profile=
|
|
456
|
+
profile=None,
|
|
455
457
|
profile_customizations=customizations_dict,
|
|
456
458
|
)
|
|
457
459
|
|
|
458
460
|
pane_sessions = [panes[name] for name in pane_names[:worker_count]]
|
|
459
461
|
|
|
460
|
-
#
|
|
462
|
+
# Pre-calculate agent types for each worker
|
|
461
463
|
import asyncio
|
|
462
464
|
|
|
463
|
-
|
|
465
|
+
agent_types: list[str] = []
|
|
466
|
+
|
|
467
|
+
for i, w in enumerate(workers):
|
|
468
|
+
agent_type = w.get("agent_type", "claude")
|
|
469
|
+
agent_types.append(agent_type)
|
|
470
|
+
|
|
471
|
+
# Start agent in all panes (both Claude and Codex)
|
|
472
|
+
async def start_agent_for_worker(index: int) -> None:
|
|
464
473
|
session = pane_sessions[index]
|
|
465
474
|
project_path = resolved_paths[index]
|
|
466
475
|
worker_config = workers[index]
|
|
467
476
|
marker_id = session_ids[index]
|
|
477
|
+
agent_type = agent_types[index]
|
|
468
478
|
|
|
469
479
|
# Check for worktree and set BEADS_DIR if needed
|
|
470
480
|
beads_dir = get_worktree_beads_dir(project_path)
|
|
471
481
|
env = {"BEADS_DIR": beads_dir} if beads_dir else None
|
|
472
482
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
483
|
+
if agent_type == "codex":
|
|
484
|
+
# Start Codex in interactive mode using start_agent_in_session
|
|
485
|
+
cli = get_cli_backend("codex")
|
|
486
|
+
await start_agent_in_session(
|
|
487
|
+
session=session,
|
|
488
|
+
cli=cli,
|
|
489
|
+
project_path=project_path,
|
|
490
|
+
dangerously_skip_permissions=worker_config.get("skip_permissions", False),
|
|
491
|
+
env=env,
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
# For Claude: use start_claude_in_session (convenience wrapper)
|
|
495
|
+
await start_claude_in_session(
|
|
496
|
+
session=session,
|
|
497
|
+
project_path=project_path,
|
|
498
|
+
dangerously_skip_permissions=worker_config.get("skip_permissions", False),
|
|
499
|
+
env=env,
|
|
500
|
+
stop_hook_marker_id=marker_id,
|
|
501
|
+
)
|
|
480
502
|
|
|
481
|
-
await asyncio.gather(*[
|
|
503
|
+
await asyncio.gather(*[start_agent_for_worker(i) for i in range(worker_count)])
|
|
482
504
|
|
|
483
505
|
# Register all sessions
|
|
484
506
|
managed_sessions = []
|
|
@@ -491,35 +513,40 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
491
513
|
)
|
|
492
514
|
# Set annotation from worker config (if provided)
|
|
493
515
|
managed.coordinator_annotation = workers[i].get("annotation")
|
|
516
|
+
# Set agent type
|
|
517
|
+
managed.agent_type = agent_types[i]
|
|
494
518
|
# Store worktree info if applicable
|
|
495
519
|
if i in worktree_paths:
|
|
496
520
|
managed.worktree_path = worktree_paths[i]
|
|
497
521
|
managed.main_repo_path = main_repo_paths[i]
|
|
498
522
|
managed_sessions.append(managed)
|
|
499
523
|
|
|
500
|
-
# Send marker messages for JSONL correlation
|
|
524
|
+
# Send marker messages for JSONL correlation (Claude only)
|
|
525
|
+
# Codex doesn't use JSONL markers for session tracking
|
|
501
526
|
for i, managed in enumerate(managed_sessions):
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
527
|
+
if managed.agent_type == "claude":
|
|
528
|
+
marker_message = generate_marker_message(
|
|
529
|
+
managed.session_id,
|
|
530
|
+
iterm_session_id=managed.iterm_session.session_id,
|
|
531
|
+
)
|
|
532
|
+
await send_prompt(pane_sessions[i], marker_message, submit=True)
|
|
507
533
|
|
|
508
|
-
# Wait for markers to appear in JSONL
|
|
534
|
+
# Wait for markers to appear in JSONL (Claude only)
|
|
509
535
|
for i, managed in enumerate(managed_sessions):
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if claude_session_id:
|
|
517
|
-
managed.claude_session_id = claude_session_id
|
|
518
|
-
else:
|
|
519
|
-
logger.warning(
|
|
520
|
-
f"Marker polling timed out for {managed.session_id}, "
|
|
521
|
-
"JSONL correlation unavailable"
|
|
536
|
+
if managed.agent_type == "claude":
|
|
537
|
+
claude_session_id = await await_marker_in_jsonl(
|
|
538
|
+
managed.project_path,
|
|
539
|
+
managed.session_id,
|
|
540
|
+
timeout=30.0,
|
|
541
|
+
poll_interval=0.1,
|
|
522
542
|
)
|
|
543
|
+
if claude_session_id:
|
|
544
|
+
managed.claude_session_id = claude_session_id
|
|
545
|
+
else:
|
|
546
|
+
logger.warning(
|
|
547
|
+
f"Marker polling timed out for {managed.session_id}, "
|
|
548
|
+
"JSONL correlation unavailable"
|
|
549
|
+
)
|
|
523
550
|
|
|
524
551
|
# Send worker prompts - always use generate_worker_prompt with bead/custom_prompt
|
|
525
552
|
workers_awaiting_task: list[str] = [] # Workers with no bead and no prompt
|
|
@@ -536,12 +563,17 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
536
563
|
worker_prompt = generate_worker_prompt(
|
|
537
564
|
managed.session_id,
|
|
538
565
|
resolved_names[i],
|
|
566
|
+
agent_type=managed.agent_type,
|
|
539
567
|
use_worktree=use_worktree,
|
|
540
568
|
bead=bead,
|
|
541
569
|
custom_prompt=custom_prompt,
|
|
542
570
|
)
|
|
543
571
|
|
|
544
|
-
|
|
572
|
+
# Send prompt to the already-running agent (both Claude and Codex)
|
|
573
|
+
# Use agent-specific timing (Codex needs longer delay before Enter)
|
|
574
|
+
logger.info(f"Sending prompt to {managed.name} (agent_type={managed.agent_type}, chars={len(worker_prompt)})")
|
|
575
|
+
await send_prompt_for_agent(pane_sessions[i], worker_prompt, agent_type=managed.agent_type)
|
|
576
|
+
logger.info(f"Prompt sent to {managed.name}")
|
|
545
577
|
|
|
546
578
|
# Mark sessions ready
|
|
547
579
|
result_sessions = {}
|
|
@@ -571,6 +603,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
571
603
|
|
|
572
604
|
worker_summaries.append({
|
|
573
605
|
"name": name,
|
|
606
|
+
"agent_type": agent_types[i],
|
|
574
607
|
"bead": bead,
|
|
575
608
|
"custom_prompt": custom_prompt,
|
|
576
609
|
"awaiting_task": awaiting,
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
Shared constants for Claude Team MCP tools.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
5
7
|
# Default page size for conversation history pagination
|
|
6
8
|
CONVERSATION_PAGE_SIZE = 5
|
|
7
9
|
|
|
10
|
+
# Directory for Codex JSONL output capture
|
|
11
|
+
# Codex streams JSONL to stdout; we pipe it through tee to this directory
|
|
12
|
+
CODEX_JSONL_DIR = Path.home() / ".claude-team" / "codex"
|
|
13
|
+
|
|
8
14
|
# Hint appended to messages sent to workers
|
|
9
15
|
WORKER_MESSAGE_HINT = "\n\n---\n(Note: Use the `bd_help` tool for guidance on using beads to track progress and add comments.)"
|
|
10
16
|
|
claude_team_mcp/worker_prompt.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Worker pre-prompt generation for coordinated team sessions."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
|
|
5
|
+
# Valid agent types for prompt generation
|
|
6
|
+
AgentType = Literal["claude", "codex"]
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def generate_worker_prompt(
|
|
7
10
|
session_id: str,
|
|
8
11
|
name: str,
|
|
12
|
+
*,
|
|
13
|
+
agent_type: AgentType = "claude",
|
|
9
14
|
use_worktree: bool = False,
|
|
10
15
|
bead: Optional[str] = None,
|
|
11
16
|
custom_prompt: Optional[str] = None,
|
|
@@ -15,6 +20,7 @@ def generate_worker_prompt(
|
|
|
15
20
|
Args:
|
|
16
21
|
session_id: The unique identifier for this worker session
|
|
17
22
|
name: The friendly name assigned to this worker
|
|
23
|
+
agent_type: The type of agent CLI ("claude" or "codex")
|
|
18
24
|
use_worktree: Whether this worker is in an isolated worktree
|
|
19
25
|
bead: Optional beads issue ID (if provided, this is the assignment)
|
|
20
26
|
custom_prompt: Optional additional instructions from the coordinator
|
|
@@ -25,7 +31,50 @@ def generate_worker_prompt(
|
|
|
25
31
|
Note:
|
|
26
32
|
The iTerm-specific marker for session recovery is emitted separately
|
|
27
33
|
via generate_marker_message() in session_state.py, which is called
|
|
28
|
-
before the worker prompt is sent.
|
|
34
|
+
before the worker prompt is sent. This marker is only used for Claude
|
|
35
|
+
workers since Codex doesn't parse JSONL markers.
|
|
36
|
+
"""
|
|
37
|
+
if agent_type == "codex":
|
|
38
|
+
return _generate_codex_worker_prompt(
|
|
39
|
+
session_id=session_id,
|
|
40
|
+
name=name,
|
|
41
|
+
use_worktree=use_worktree,
|
|
42
|
+
bead=bead,
|
|
43
|
+
custom_prompt=custom_prompt,
|
|
44
|
+
)
|
|
45
|
+
# Default to Claude prompt for unknown agent types to maintain backward compatibility
|
|
46
|
+
return _generate_claude_worker_prompt(
|
|
47
|
+
session_id=session_id,
|
|
48
|
+
name=name,
|
|
49
|
+
use_worktree=use_worktree,
|
|
50
|
+
bead=bead,
|
|
51
|
+
custom_prompt=custom_prompt,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _generate_claude_worker_prompt(
|
|
56
|
+
session_id: str,
|
|
57
|
+
name: str,
|
|
58
|
+
use_worktree: bool = False,
|
|
59
|
+
bead: Optional[str] = None,
|
|
60
|
+
custom_prompt: Optional[str] = None,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Generate the pre-prompt for a Claude Code worker session.
|
|
63
|
+
|
|
64
|
+
Claude Code workers have access to:
|
|
65
|
+
- claude-team MCP markers for session recovery
|
|
66
|
+
- Stop hook idle detection
|
|
67
|
+
- Full MCP tool ecosystem
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
session_id: The unique identifier for this worker session
|
|
71
|
+
name: The friendly name assigned to this worker
|
|
72
|
+
use_worktree: Whether this worker is in an isolated worktree
|
|
73
|
+
bead: Optional beads issue ID
|
|
74
|
+
custom_prompt: Optional additional instructions
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Formatted pre-prompt for Claude worker
|
|
29
78
|
"""
|
|
30
79
|
# Build optional sections with dynamic numbering
|
|
31
80
|
next_step = 4
|
|
@@ -107,6 +156,111 @@ your session as idle to the coordinator so they can respond.
|
|
|
107
156
|
'''
|
|
108
157
|
|
|
109
158
|
|
|
159
|
+
def _generate_codex_worker_prompt(
|
|
160
|
+
session_id: str,
|
|
161
|
+
name: str,
|
|
162
|
+
use_worktree: bool = False,
|
|
163
|
+
bead: Optional[str] = None,
|
|
164
|
+
custom_prompt: Optional[str] = None,
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Generate the pre-prompt for an OpenAI Codex worker session.
|
|
167
|
+
|
|
168
|
+
Codex workers differ from Claude:
|
|
169
|
+
- No claude-team MCP markers (Codex doesn't parse JSONL markers)
|
|
170
|
+
- No Stop hook idle detection (uses output pattern matching or timeouts)
|
|
171
|
+
- Runs with --full-auto instead of --dangerously-skip-permissions
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
session_id: The unique identifier for this worker session
|
|
175
|
+
name: The friendly name assigned to this worker
|
|
176
|
+
use_worktree: Whether this worker is in an isolated worktree
|
|
177
|
+
bead: Optional beads issue ID
|
|
178
|
+
custom_prompt: Optional additional instructions
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Formatted pre-prompt for Codex worker
|
|
182
|
+
"""
|
|
183
|
+
# Build optional sections with dynamic numbering
|
|
184
|
+
next_step = 4
|
|
185
|
+
extra_sections = ""
|
|
186
|
+
|
|
187
|
+
# Beads section (if bead provided) - same workflow as Claude
|
|
188
|
+
if bead:
|
|
189
|
+
beads_section = f"""
|
|
190
|
+
{next_step}. **Beads workflow.** You're working on `{bead}`. Follow this workflow:
|
|
191
|
+
- Mark in progress: `bd --no-db update {bead} --status in_progress`
|
|
192
|
+
- Implement the changes
|
|
193
|
+
- Close issue: `bd --no-db close {bead}`
|
|
194
|
+
- Commit with issue reference: `git add -A && git commit -m "{bead}: <summary>"`
|
|
195
|
+
|
|
196
|
+
Use `bd --no-db` for all beads commands (required in worktrees).
|
|
197
|
+
"""
|
|
198
|
+
extra_sections += beads_section
|
|
199
|
+
next_step += 1
|
|
200
|
+
|
|
201
|
+
# Commit section (if worktree but beads section didn't already cover commit)
|
|
202
|
+
if use_worktree and not bead:
|
|
203
|
+
commit_section = f"""
|
|
204
|
+
{next_step}. **Commit when done.** You're in an isolated worktree branch — commit your
|
|
205
|
+
completed work so it can be easily cherry-picked or merged. Use a clear
|
|
206
|
+
commit message summarizing what you did. Don't push; the coordinator
|
|
207
|
+
handles that.
|
|
208
|
+
"""
|
|
209
|
+
extra_sections += commit_section
|
|
210
|
+
|
|
211
|
+
# Closing/assignment section - 4 cases based on bead and custom_prompt
|
|
212
|
+
if bead and custom_prompt:
|
|
213
|
+
closing = f"""=== YOUR ASSIGNMENT ===
|
|
214
|
+
|
|
215
|
+
The coordinator assigned you `{bead}` (use `bd show {bead}` for details) and included
|
|
216
|
+
the following instructions:
|
|
217
|
+
|
|
218
|
+
{custom_prompt}
|
|
219
|
+
|
|
220
|
+
Get to work!"""
|
|
221
|
+
elif bead:
|
|
222
|
+
closing = f"""=== YOUR ASSIGNMENT ===
|
|
223
|
+
|
|
224
|
+
Your assignment is `{bead}`. Use `bd show {bead}` for details. Get to work!"""
|
|
225
|
+
elif custom_prompt:
|
|
226
|
+
closing = f"""=== YOUR ASSIGNMENT ===
|
|
227
|
+
|
|
228
|
+
The coordinator assigned you the following task:
|
|
229
|
+
|
|
230
|
+
{custom_prompt}
|
|
231
|
+
|
|
232
|
+
Get to work!"""
|
|
233
|
+
else:
|
|
234
|
+
closing = "Alright, you're all set. The coordinator will send your first task shortly."
|
|
235
|
+
|
|
236
|
+
# Codex prompt differs from Claude in key ways:
|
|
237
|
+
# - No reference to claude-team MCP markers
|
|
238
|
+
# - No "automatically report your session as idle" - Codex doesn't use stop hooks
|
|
239
|
+
# - Simpler coordination model (output-based status checking)
|
|
240
|
+
return f'''Hey {name}! Welcome to the team.
|
|
241
|
+
|
|
242
|
+
You're part of a coordinated multi-agent team. Your coordinator has tasks for you.
|
|
243
|
+
Do your best to complete the work you've been assigned autonomously.
|
|
244
|
+
|
|
245
|
+
If you have questions or concerns, clearly state them at the end of your response
|
|
246
|
+
and wait for further instructions. The coordinator will check your progress periodically.
|
|
247
|
+
|
|
248
|
+
=== THE DEAL ===
|
|
249
|
+
|
|
250
|
+
1. **Do the work fully.** Either complete it or explain what's blocking you.
|
|
251
|
+
The coordinator reads your output to understand what happened.
|
|
252
|
+
|
|
253
|
+
2. **When you're done,** leave a clear summary of what you accomplished.
|
|
254
|
+
End your response with "COMPLETED" or "BLOCKED: <reason>" so the coordinator
|
|
255
|
+
can easily assess your status.
|
|
256
|
+
|
|
257
|
+
3. **If blocked,** explain what you need. The coordinator will read your output
|
|
258
|
+
and address it.
|
|
259
|
+
{extra_sections}
|
|
260
|
+
{closing}
|
|
261
|
+
'''
|
|
262
|
+
|
|
263
|
+
|
|
110
264
|
def get_coordinator_guidance(
|
|
111
265
|
worker_summaries: list[dict],
|
|
112
266
|
) -> str:
|
|
@@ -115,6 +269,7 @@ def get_coordinator_guidance(
|
|
|
115
269
|
Args:
|
|
116
270
|
worker_summaries: List of dicts with keys:
|
|
117
271
|
- name: Worker name
|
|
272
|
+
- agent_type: Agent type ("claude" or "codex")
|
|
118
273
|
- bead: Optional bead ID
|
|
119
274
|
- custom_prompt: Optional custom instructions (truncated for display)
|
|
120
275
|
- awaiting_task: True if worker has no bead and no prompt
|
|
@@ -122,43 +277,74 @@ def get_coordinator_guidance(
|
|
|
122
277
|
Returns:
|
|
123
278
|
Formatted coordinator guidance string
|
|
124
279
|
"""
|
|
280
|
+
# Check if we have a mixed team
|
|
281
|
+
agent_types = {w.get("agent_type", "claude") for w in worker_summaries}
|
|
282
|
+
is_mixed_team = len(agent_types) > 1
|
|
283
|
+
|
|
125
284
|
# Build per-worker summary lines
|
|
126
285
|
worker_lines = []
|
|
127
286
|
for w in worker_summaries:
|
|
128
287
|
name = w["name"]
|
|
288
|
+
agent_type = w.get("agent_type", "claude")
|
|
129
289
|
bead = w.get("bead")
|
|
130
290
|
custom_prompt = w.get("custom_prompt")
|
|
131
291
|
awaiting = w.get("awaiting_task", False)
|
|
132
292
|
|
|
293
|
+
# Add agent type indicator if mixed team
|
|
294
|
+
type_indicator = f" [{agent_type}]" if is_mixed_team else ""
|
|
295
|
+
|
|
133
296
|
if awaiting:
|
|
134
|
-
worker_lines.append(
|
|
297
|
+
worker_lines.append(
|
|
298
|
+
f"- **{name}**{type_indicator}: "
|
|
299
|
+
"AWAITING TASK - send them instructions now"
|
|
300
|
+
)
|
|
135
301
|
elif bead and custom_prompt:
|
|
136
302
|
# Truncate custom prompt for display
|
|
137
|
-
short_prompt =
|
|
138
|
-
|
|
303
|
+
short_prompt = (
|
|
304
|
+
custom_prompt[:50] + "..." if len(custom_prompt) > 50 else custom_prompt
|
|
305
|
+
)
|
|
306
|
+
worker_lines.append(
|
|
307
|
+
f"- **{name}**{type_indicator}: `{bead}` + custom: \"{short_prompt}\""
|
|
308
|
+
)
|
|
139
309
|
elif bead:
|
|
140
|
-
worker_lines.append(
|
|
310
|
+
worker_lines.append(
|
|
311
|
+
f"- **{name}**{type_indicator}: `{bead}` "
|
|
312
|
+
"(beads workflow: mark in_progress -> implement -> close -> commit)"
|
|
313
|
+
)
|
|
141
314
|
elif custom_prompt:
|
|
142
|
-
short_prompt =
|
|
143
|
-
|
|
315
|
+
short_prompt = (
|
|
316
|
+
custom_prompt[:50] + "..." if len(custom_prompt) > 50 else custom_prompt
|
|
317
|
+
)
|
|
318
|
+
worker_lines.append(
|
|
319
|
+
f"- **{name}**{type_indicator}: custom task: \"{short_prompt}\""
|
|
320
|
+
)
|
|
144
321
|
|
|
145
322
|
workers_section = "\n".join(worker_lines)
|
|
146
323
|
|
|
324
|
+
# Build mixed team guidance if applicable
|
|
325
|
+
mixed_team_section = ""
|
|
326
|
+
if is_mixed_team:
|
|
327
|
+
mixed_team_section = """
|
|
328
|
+
**Mixed team note:** You have both Claude and Codex workers:
|
|
329
|
+
- **Claude workers**: Idle detection via Stop hooks (automatic)
|
|
330
|
+
- **Codex workers**: Check status by reading their output for "COMPLETED" or "BLOCKED"
|
|
331
|
+
"""
|
|
332
|
+
|
|
147
333
|
return f"""=== TEAM DISPATCHED ===
|
|
148
334
|
|
|
149
335
|
{workers_section}
|
|
150
|
-
|
|
336
|
+
{mixed_team_section}
|
|
151
337
|
Workers will do the work and explain their output. If blocked, they'll say so.
|
|
152
338
|
You review everything before it's considered done.
|
|
153
339
|
|
|
154
340
|
**Coordination style reminder:** Match your approach to the task. Hands-off for exploratory
|
|
155
341
|
work (check in when asked), autonomous for pipelines (wait for completion, read logs, continue).
|
|
156
342
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
1.
|
|
160
|
-
2.
|
|
161
|
-
3.
|
|
343
|
+
**WORKTREE LIFECYCLE** — Workers with worktrees commit to ephemeral branches.
|
|
344
|
+
When you close workers:
|
|
345
|
+
1. Worktree directories are removed, but branches (and commits) are preserved
|
|
346
|
+
2. Review commits on worker branches before merging
|
|
347
|
+
3. Merge or cherry-pick to main, then delete the worker branch
|
|
162
348
|
|
|
163
|
-
|
|
349
|
+
Branches persist until explicitly deleted with `git branch -d <branch>`.
|
|
164
350
|
"""
|
claude_team_mcp/worktree.py
CHANGED
|
@@ -240,6 +240,10 @@ def create_local_worktree(
|
|
|
240
240
|
The branch name matches the worktree directory name for consistency.
|
|
241
241
|
Automatically adds .worktrees to .gitignore if not present.
|
|
242
242
|
|
|
243
|
+
If a worktree path or branch already exists, appends an incrementing
|
|
244
|
+
suffix (-1, -2, etc.) until an available name is found. This allows
|
|
245
|
+
multiple workers to work on the same bead in parallel.
|
|
246
|
+
|
|
243
247
|
Args:
|
|
244
248
|
repo_path: Path to the main repository
|
|
245
249
|
worker_name: Name of the worker (used in fallback naming)
|
|
@@ -262,6 +266,9 @@ def create_local_worktree(
|
|
|
262
266
|
)
|
|
263
267
|
# Returns: Path("/path/to/repo/.worktrees/cic-abc-add-local-worktrees")
|
|
264
268
|
|
|
269
|
+
# If called again with same bead/annotation:
|
|
270
|
+
# Returns: Path("/path/to/repo/.worktrees/cic-abc-add-local-worktrees-1")
|
|
271
|
+
|
|
265
272
|
# Without bead ID
|
|
266
273
|
path = create_local_worktree(
|
|
267
274
|
repo_path=Path("/path/to/repo"),
|
|
@@ -290,7 +297,6 @@ def create_local_worktree(
|
|
|
290
297
|
|
|
291
298
|
# Worktree path inside the repo
|
|
292
299
|
worktrees_dir = repo_path / LOCAL_WORKTREE_DIR
|
|
293
|
-
worktree_path = worktrees_dir / dir_name
|
|
294
300
|
|
|
295
301
|
# Ensure .worktrees is in .gitignore
|
|
296
302
|
ensure_gitignore_entry(repo_path, LOCAL_WORKTREE_DIR)
|
|
@@ -298,29 +304,32 @@ def create_local_worktree(
|
|
|
298
304
|
# Ensure .worktrees directory exists
|
|
299
305
|
worktrees_dir.mkdir(parents=True, exist_ok=True)
|
|
300
306
|
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
# Find an available name, handling collisions with incrementing suffix.
|
|
308
|
+
# Check both path existence and branch existence (git won't allow the same
|
|
309
|
+
# branch checked out in multiple worktrees).
|
|
310
|
+
def branch_exists(name: str) -> bool:
|
|
311
|
+
result = subprocess.run(
|
|
312
|
+
["git", "-C", str(repo_path), "rev-parse", "--verify", f"refs/heads/{name}"],
|
|
313
|
+
capture_output=True,
|
|
314
|
+
text=True,
|
|
315
|
+
)
|
|
316
|
+
return result.returncode == 0
|
|
304
317
|
|
|
305
|
-
|
|
306
|
-
|
|
318
|
+
base_dir_name = dir_name
|
|
319
|
+
worktree_path = worktrees_dir / dir_name
|
|
320
|
+
suffix = 0
|
|
307
321
|
|
|
308
|
-
|
|
309
|
-
|
|
322
|
+
while worktree_path.exists() or branch_exists(dir_name):
|
|
323
|
+
suffix += 1
|
|
324
|
+
dir_name = f"{base_dir_name}-{suffix}"
|
|
325
|
+
worktree_path = worktrees_dir / dir_name
|
|
310
326
|
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
["git", "-C", str(repo_path), "rev-parse", "--verify", f"refs/heads/{branch_name}"],
|
|
314
|
-
capture_output=True,
|
|
315
|
-
text=True,
|
|
316
|
-
)
|
|
327
|
+
# Branch name matches directory name for clarity
|
|
328
|
+
branch_name = dir_name
|
|
317
329
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
else:
|
|
322
|
-
# Branch doesn't exist, create it with -b
|
|
323
|
-
cmd.extend(["-b", branch_name, str(worktree_path)])
|
|
330
|
+
# Build the git worktree add command.
|
|
331
|
+
# Branch is guaranteed not to exist (collision loop checked for it).
|
|
332
|
+
cmd = ["git", "-C", str(repo_path), "worktree", "add", "-b", branch_name, str(worktree_path)]
|
|
324
333
|
|
|
325
334
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
326
335
|
|