claude-team-mcp 0.4.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.
Files changed (42) hide show
  1. claude_team_mcp/__init__.py +24 -0
  2. claude_team_mcp/__main__.py +8 -0
  3. claude_team_mcp/cli_backends/__init__.py +44 -0
  4. claude_team_mcp/cli_backends/base.py +132 -0
  5. claude_team_mcp/cli_backends/claude.py +110 -0
  6. claude_team_mcp/cli_backends/codex.py +110 -0
  7. claude_team_mcp/colors.py +108 -0
  8. claude_team_mcp/formatting.py +120 -0
  9. claude_team_mcp/idle_detection.py +488 -0
  10. claude_team_mcp/iterm_utils.py +1119 -0
  11. claude_team_mcp/names.py +427 -0
  12. claude_team_mcp/profile.py +364 -0
  13. claude_team_mcp/registry.py +426 -0
  14. claude_team_mcp/schemas/__init__.py +5 -0
  15. claude_team_mcp/schemas/codex.py +267 -0
  16. claude_team_mcp/server.py +390 -0
  17. claude_team_mcp/session_state.py +1058 -0
  18. claude_team_mcp/subprocess_cache.py +119 -0
  19. claude_team_mcp/tools/__init__.py +52 -0
  20. claude_team_mcp/tools/adopt_worker.py +122 -0
  21. claude_team_mcp/tools/annotate_worker.py +57 -0
  22. claude_team_mcp/tools/bd_help.py +42 -0
  23. claude_team_mcp/tools/check_idle_workers.py +98 -0
  24. claude_team_mcp/tools/close_workers.py +194 -0
  25. claude_team_mcp/tools/discover_workers.py +129 -0
  26. claude_team_mcp/tools/examine_worker.py +56 -0
  27. claude_team_mcp/tools/list_workers.py +76 -0
  28. claude_team_mcp/tools/list_worktrees.py +106 -0
  29. claude_team_mcp/tools/message_workers.py +311 -0
  30. claude_team_mcp/tools/read_worker_logs.py +158 -0
  31. claude_team_mcp/tools/spawn_workers.py +634 -0
  32. claude_team_mcp/tools/wait_idle_workers.py +148 -0
  33. claude_team_mcp/utils/__init__.py +17 -0
  34. claude_team_mcp/utils/constants.py +87 -0
  35. claude_team_mcp/utils/errors.py +87 -0
  36. claude_team_mcp/utils/worktree_detection.py +79 -0
  37. claude_team_mcp/worker_prompt.py +350 -0
  38. claude_team_mcp/worktree.py +532 -0
  39. claude_team_mcp-0.4.0.dist-info/METADATA +414 -0
  40. claude_team_mcp-0.4.0.dist-info/RECORD +42 -0
  41. claude_team_mcp-0.4.0.dist-info/WHEEL +4 -0
  42. claude_team_mcp-0.4.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,24 @@
1
+ """
2
+ Claude Team MCP Server
3
+
4
+ An MCP server that allows one Claude Code session to spawn and manage
5
+ a team of other Claude Code sessions via iTerm2.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from .colors import generate_tab_color, get_hue_for_index, hsl_to_rgb_tuple
11
+
12
+
13
+ def main():
14
+ """Entry point for the claude-team command."""
15
+ from .server import main as server_main
16
+ server_main()
17
+
18
+
19
+ __all__ = [
20
+ "main",
21
+ "generate_tab_color",
22
+ "get_hue_for_index",
23
+ "hsl_to_rgb_tuple",
24
+ ]
@@ -0,0 +1,8 @@
1
+ """
2
+ Allow running the package directly with: python -m claude_team_mcp
3
+ """
4
+
5
+ from . import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,44 @@
1
+ """
2
+ CLI Backends Module.
3
+
4
+ Provides abstraction layer for different agent CLI tools (Claude Code, Codex, etc.)
5
+ This allows claude-team to orchestrate multiple agent types through a unified interface.
6
+ """
7
+
8
+ from .base import AgentCLI
9
+ from .claude import ClaudeCLI, claude_cli
10
+ from .codex import CodexCLI, codex_cli
11
+
12
+ __all__ = [
13
+ "AgentCLI",
14
+ "ClaudeCLI",
15
+ "claude_cli",
16
+ "CodexCLI",
17
+ "codex_cli",
18
+ "get_cli_backend",
19
+ ]
20
+
21
+
22
+ def get_cli_backend(agent_type: str = "claude") -> AgentCLI:
23
+ """
24
+ Get a CLI backend instance by agent type.
25
+
26
+ Args:
27
+ agent_type: The agent type ("claude" or "codex")
28
+
29
+ Returns:
30
+ An AgentCLI implementation instance
31
+
32
+ Raises:
33
+ ValueError: If the agent type is not supported
34
+ """
35
+ backends = {
36
+ "claude": claude_cli,
37
+ "codex": codex_cli,
38
+ }
39
+
40
+ if agent_type not in backends:
41
+ valid = ", ".join(sorted(backends.keys()))
42
+ raise ValueError(f"Unknown agent type: {agent_type}. Valid types: {valid}")
43
+
44
+ return backends[agent_type]
@@ -0,0 +1,132 @@
1
+ """
2
+ Base protocol for CLI agent backends.
3
+
4
+ Defines the interface that all CLI backends (Claude, Codex, etc.) must implement.
5
+ This abstraction allows claude-team to orchestrate different agent CLIs.
6
+ """
7
+
8
+ from abc import abstractmethod
9
+ from typing import Literal, Protocol, runtime_checkable
10
+
11
+
12
+ @runtime_checkable
13
+ class AgentCLI(Protocol):
14
+ """
15
+ Protocol defining the interface for agent CLI backends.
16
+
17
+ Each implementation encapsulates the CLI-specific details:
18
+ - Command and arguments
19
+ - Ready detection patterns
20
+ - Idle/completion detection method
21
+ - Settings/hook injection support
22
+ """
23
+
24
+ @property
25
+ @abstractmethod
26
+ def engine_id(self) -> str:
27
+ """
28
+ Unique identifier for this CLI engine (e.g., "claude", "codex").
29
+
30
+ Used for configuration, logging, and distinguishing between backends.
31
+ """
32
+ ...
33
+
34
+ @abstractmethod
35
+ def command(self) -> str:
36
+ """
37
+ Return the CLI executable name or path.
38
+
39
+ Examples: "claude", "codex", "/usr/local/bin/custom-agent"
40
+ """
41
+ ...
42
+
43
+ @abstractmethod
44
+ def build_args(
45
+ self,
46
+ *,
47
+ dangerously_skip_permissions: bool = False,
48
+ settings_file: str | None = None,
49
+ ) -> list[str]:
50
+ """
51
+ Build the argument list for the CLI command.
52
+
53
+ Args:
54
+ dangerously_skip_permissions: If True, add flag to skip permission prompts
55
+ settings_file: Optional path to settings file for hook injection
56
+
57
+ Returns:
58
+ List of command-line arguments (not including the command itself)
59
+ """
60
+ ...
61
+
62
+ @abstractmethod
63
+ def ready_patterns(self) -> list[str]:
64
+ """
65
+ Return patterns that indicate the CLI is ready for input.
66
+
67
+ These patterns are searched for in terminal output to detect when
68
+ the agent has started and is ready to receive prompts.
69
+
70
+ Returns:
71
+ List of strings to search for in terminal output
72
+ """
73
+ ...
74
+
75
+ @abstractmethod
76
+ def idle_detection_method(self) -> Literal["stop_hook", "jsonl_stream", "none"]:
77
+ """
78
+ Return the method used to detect when the agent finishes responding.
79
+
80
+ - "stop_hook": Uses a Stop hook that fires when the agent completes
81
+ - "jsonl_stream": Monitors JSONL output for completion markers
82
+ - "none": No idle detection available (must use timeouts)
83
+
84
+ Returns:
85
+ The detection method identifier
86
+ """
87
+ ...
88
+
89
+ @abstractmethod
90
+ def supports_settings_file(self) -> bool:
91
+ """
92
+ Return whether this CLI supports --settings flag for hook injection.
93
+
94
+ If False, build_args() should ignore the settings_file parameter.
95
+ """
96
+ ...
97
+
98
+ def build_full_command(
99
+ self,
100
+ *,
101
+ dangerously_skip_permissions: bool = False,
102
+ settings_file: str | None = None,
103
+ env_vars: dict[str, str] | None = None,
104
+ ) -> str:
105
+ """
106
+ Build the complete command string including env vars.
107
+
108
+ This is a convenience method that combines command(), build_args(),
109
+ and optional environment variables into a single shell command string.
110
+
111
+ Args:
112
+ dangerously_skip_permissions: Skip permission prompts
113
+ settings_file: Settings file for hook injection
114
+ env_vars: Environment variables to prepend
115
+
116
+ Returns:
117
+ Complete command string ready for shell execution
118
+ """
119
+ cmd = self.command()
120
+ args = self.build_args(
121
+ dangerously_skip_permissions=dangerously_skip_permissions,
122
+ settings_file=settings_file if self.supports_settings_file() else None,
123
+ )
124
+
125
+ if args:
126
+ cmd = f"{cmd} {' '.join(args)}"
127
+
128
+ if env_vars:
129
+ env_exports = " ".join(f"{k}={v}" for k, v in env_vars.items())
130
+ cmd = f"{env_exports} {cmd}"
131
+
132
+ return cmd
@@ -0,0 +1,110 @@
1
+ """
2
+ Claude Code CLI backend.
3
+
4
+ Implements the AgentCLI protocol for Claude Code CLI.
5
+ This preserves the existing behavior from iterm_utils.py.
6
+ """
7
+
8
+ import os
9
+ from typing import Literal
10
+
11
+ from .base import AgentCLI
12
+
13
+
14
+ class ClaudeCLI(AgentCLI):
15
+ """
16
+ Claude Code CLI implementation.
17
+
18
+ Supports:
19
+ - --dangerously-skip-permissions flag
20
+ - --settings flag for Stop hook injection
21
+ - Ready detection via TUI patterns (robot banner, '>' prompt, 'tokens' status)
22
+ - Idle detection via Stop hook markers in JSONL
23
+ """
24
+
25
+ @property
26
+ def engine_id(self) -> str:
27
+ """Return 'claude' as the engine identifier."""
28
+ return "claude"
29
+
30
+ def command(self) -> str:
31
+ """
32
+ Return the Claude CLI command.
33
+
34
+ Respects CLAUDE_TEAM_COMMAND environment variable for overrides
35
+ (e.g., "happy" wrapper).
36
+ """
37
+ return os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
38
+
39
+ def build_args(
40
+ self,
41
+ *,
42
+ dangerously_skip_permissions: bool = False,
43
+ settings_file: str | None = None,
44
+ ) -> list[str]:
45
+ """
46
+ Build Claude CLI arguments.
47
+
48
+ Args:
49
+ dangerously_skip_permissions: Add --dangerously-skip-permissions
50
+ settings_file: Path to settings JSON for Stop hook injection
51
+
52
+ Returns:
53
+ List of CLI arguments
54
+ """
55
+ args: list[str] = []
56
+
57
+ if dangerously_skip_permissions:
58
+ args.append("--dangerously-skip-permissions")
59
+
60
+ # Only add --settings for the default 'claude' command.
61
+ # Custom commands like 'happy' have their own session tracking mechanisms.
62
+ # See HAPPY_INTEGRATION_RESEARCH.md for full analysis.
63
+ if settings_file and self._is_default_command():
64
+ args.append("--settings")
65
+ args.append(settings_file)
66
+
67
+ return args
68
+
69
+ def ready_patterns(self) -> list[str]:
70
+ """
71
+ Return patterns indicating Claude TUI is ready.
72
+
73
+ These patterns appear in Claude's startup:
74
+ - '>' prompt indicates input ready
75
+ - 'tokens' in status bar
76
+ - Parts of the robot ASCII art banner
77
+ """
78
+ return [
79
+ ">", # Input prompt
80
+ "tokens", # Status bar
81
+ "Claude Code v", # Version line in banner
82
+ "▐▛███▜▌", # Top of robot head
83
+ "▝▜█████▛▘", # Middle of robot
84
+ ]
85
+
86
+ def idle_detection_method(self) -> Literal["stop_hook", "jsonl_stream", "none"]:
87
+ """
88
+ Claude uses Stop hook for idle detection.
89
+
90
+ A Stop hook writes a marker to the JSONL when Claude finishes responding.
91
+ """
92
+ return "stop_hook"
93
+
94
+ def supports_settings_file(self) -> bool:
95
+ """
96
+ Claude supports --settings for hook injection.
97
+
98
+ Only returns True for the default 'claude' command.
99
+ Custom wrappers may have their own settings mechanisms.
100
+ """
101
+ return self._is_default_command()
102
+
103
+ def _is_default_command(self) -> bool:
104
+ """Check if using the default 'claude' command (not a custom wrapper)."""
105
+ cmd = os.environ.get("CLAUDE_TEAM_COMMAND", "claude")
106
+ return cmd == "claude"
107
+
108
+
109
+ # Singleton instance for convenience
110
+ claude_cli = ClaudeCLI()
@@ -0,0 +1,110 @@
1
+ """
2
+ OpenAI Codex CLI backend.
3
+
4
+ Implements the AgentCLI protocol for OpenAI's Codex CLI.
5
+ This is a basic implementation - full integration will be done in later tasks.
6
+
7
+ Codex CLI reference: https://github.com/openai/codex
8
+ """
9
+
10
+ import os
11
+ from typing import Literal
12
+
13
+ from .base import AgentCLI
14
+
15
+
16
+ class CodexCLI(AgentCLI):
17
+ """
18
+ OpenAI Codex CLI implementation.
19
+
20
+ Note: This is a basic structure. Full Codex integration (ready detection,
21
+ idle detection, etc.) will be implemented in later tasks (cic-f7w.3+).
22
+
23
+ Codex CLI characteristics:
24
+ - Uses `codex` command
25
+ - Has --full-auto flag for non-interactive mode
26
+ - No known Stop hook equivalent (may need JSONL streaming or timeouts)
27
+ """
28
+
29
+ @property
30
+ def engine_id(self) -> str:
31
+ """Return 'codex' as the engine identifier."""
32
+ return "codex"
33
+
34
+ def command(self) -> str:
35
+ """
36
+ Return the Codex CLI command.
37
+
38
+ Respects CLAUDE_TEAM_CODEX_COMMAND environment variable for overrides
39
+ (e.g., "happy codex" wrapper).
40
+ """
41
+ return os.environ.get("CLAUDE_TEAM_CODEX_COMMAND", "codex")
42
+
43
+ def build_args(
44
+ self,
45
+ *,
46
+ dangerously_skip_permissions: bool = False,
47
+ settings_file: str | None = None,
48
+ ) -> list[str]:
49
+ """
50
+ Build Codex CLI arguments for interactive mode.
51
+
52
+ Args:
53
+ dangerously_skip_permissions: Maps to --full-auto for Codex
54
+ settings_file: Ignored - Codex doesn't support settings injection
55
+
56
+ Returns:
57
+ List of CLI arguments for interactive mode
58
+ """
59
+ args: list[str] = []
60
+
61
+ # Codex uses --dangerously-bypass-approvals-and-sandbox for autonomous operation
62
+ # (--full-auto doesn't work through happy wrapper)
63
+ if dangerously_skip_permissions:
64
+ args.append("--dangerously-bypass-approvals-and-sandbox")
65
+
66
+ # Note: settings_file is ignored - Codex doesn't support this
67
+ # Idle detection uses session file polling instead
68
+
69
+ return args
70
+
71
+
72
+ def ready_patterns(self) -> list[str]:
73
+ """
74
+ Return patterns indicating Codex CLI is ready for input.
75
+
76
+ Codex in interactive mode shows status bar when ready.
77
+ Updated for Codex CLI v0.80.0 behavior.
78
+ """
79
+ return [
80
+ "context left", # Status bar shows "100% context left"
81
+ "for shortcuts", # Status bar shows "? for shortcuts"
82
+ "What can I help you with?", # Legacy prompt (older versions)
83
+ "codex>", # Alternative prompt pattern
84
+ "»", # Codex uses this prompt symbol
85
+ "Waiting for messages", # Happy codex wrapper
86
+ "Codex Agent Running", # Happy codex status bar
87
+ ]
88
+
89
+ def idle_detection_method(self) -> Literal["stop_hook", "jsonl_stream", "none"]:
90
+ """
91
+ Codex idle detection method.
92
+
93
+ Codex writes session files to ~/.codex/sessions/YYYY/MM/DD/.
94
+ The idle_detection module polls these files for agent_message
95
+ events which indicate the agent has finished responding.
96
+ """
97
+ return "jsonl_stream"
98
+
99
+ def supports_settings_file(self) -> bool:
100
+ """
101
+ Codex doesn't support --settings for hook injection.
102
+
103
+ Alternative completion detection methods will be needed.
104
+ """
105
+ return False
106
+
107
+
108
+
109
+ # Singleton instance for convenience
110
+ codex_cli = CodexCLI()
@@ -0,0 +1,108 @@
1
+ """
2
+ Dynamic tab color generation for iTerm2 sessions.
3
+
4
+ Generates visually distinct colors using the golden ratio for hue distribution,
5
+ ensuring each session gets a unique, easily distinguishable tab color.
6
+ """
7
+
8
+ import colorsys
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from iterm2.color import Color as ItermColor
13
+
14
+
15
+ # Golden ratio conjugate (φ - 1), used for even hue distribution
16
+ GOLDEN_RATIO_CONJUGATE = 0.618033988749895
17
+
18
+ # Default saturation and lightness values for tab colors (HSL)
19
+ DEFAULT_SATURATION = 0.65 # 65%
20
+ DEFAULT_LIGHTNESS = 0.55 # 55%
21
+
22
+
23
+ def generate_tab_color(
24
+ index: int,
25
+ saturation: float = DEFAULT_SATURATION,
26
+ lightness: float = DEFAULT_LIGHTNESS,
27
+ ) -> "ItermColor":
28
+ """
29
+ Generate a distinct tab color for a given index.
30
+
31
+ Uses the golden ratio to distribute hues evenly across the color wheel,
32
+ ensuring that consecutively spawned sessions have visually distinct colors.
33
+ The golden ratio approach avoids clustering that can occur with linear
34
+ hue increments.
35
+
36
+ Args:
37
+ index: The session index (0-based). Each index produces a unique hue.
38
+ saturation: HSL saturation value (0.0-1.0). Default 0.65 (65%).
39
+ lightness: HSL lightness value (0.0-1.0). Default 0.55 (55%).
40
+
41
+ Returns:
42
+ iterm2.Color object ready to use with tab color APIs.
43
+
44
+ Example:
45
+ # First session gets a warm orange-red
46
+ color0 = generate_tab_color(0)
47
+
48
+ # Second session gets a contrasting blue-green
49
+ color1 = generate_tab_color(1)
50
+
51
+ # Colors remain visually distinct even for many sessions
52
+ color10 = generate_tab_color(10)
53
+ """
54
+ from iterm2.color import Color
55
+
56
+ # Calculate hue using golden ratio distribution
57
+ # Multiply index by golden ratio conjugate and take fractional part
58
+ # This distributes hues evenly across the color wheel
59
+ hue = (index * GOLDEN_RATIO_CONJUGATE) % 1.0
60
+
61
+ # Convert HSL to RGB (colorsys uses HLS ordering: hue, lightness, saturation)
62
+ r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
63
+
64
+ # iterm2.Color expects integer RGB values 0-255
65
+ return Color(
66
+ r=int(r * 255),
67
+ g=int(g * 255),
68
+ b=int(b * 255),
69
+ )
70
+
71
+
72
+ def hsl_to_rgb_tuple(
73
+ hue: float,
74
+ saturation: float = DEFAULT_SATURATION,
75
+ lightness: float = DEFAULT_LIGHTNESS,
76
+ ) -> tuple[int, int, int]:
77
+ """
78
+ Convert HSL values to RGB tuple (0-255 range).
79
+
80
+ Utility function for cases where raw RGB values are needed
81
+ instead of an iterm2.Color object.
82
+
83
+ Args:
84
+ hue: HSL hue value (0.0-1.0)
85
+ saturation: HSL saturation value (0.0-1.0)
86
+ lightness: HSL lightness value (0.0-1.0)
87
+
88
+ Returns:
89
+ Tuple of (red, green, blue) integers in 0-255 range.
90
+ """
91
+ # colorsys uses HLS (hue, lightness, saturation) ordering
92
+ r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
93
+ return (int(r * 255), int(g * 255), int(b * 255))
94
+
95
+
96
+ def get_hue_for_index(index: int) -> float:
97
+ """
98
+ Get the hue value (0.0-1.0) for a given index.
99
+
100
+ Useful when you need just the hue for custom color manipulation.
101
+
102
+ Args:
103
+ index: The session index (0-based)
104
+
105
+ Returns:
106
+ Hue value in range 0.0 to 1.0
107
+ """
108
+ return (index * GOLDEN_RATIO_CONJUGATE) % 1.0
@@ -0,0 +1,120 @@
1
+ """
2
+ Formatting utilities for Claude Team MCP.
3
+
4
+ Provides functions for formatting session titles, badge text, and other
5
+ display strings used in iTerm2 tabs and UI badges.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+
11
+ def format_session_title(
12
+ session_name: str,
13
+ issue_id: Optional[str] = None,
14
+ annotation: Optional[str] = None,
15
+ ) -> str:
16
+ """
17
+ Format a session title for iTerm2 tab display.
18
+
19
+ Creates a formatted title string combining session name, optional issue ID,
20
+ and optional annotation.
21
+
22
+ Args:
23
+ session_name: Session identifier (e.g., "worker-1")
24
+ issue_id: Optional issue/ticket ID (e.g., "cic-3dj")
25
+ annotation: Optional task annotation (e.g., "profile module")
26
+
27
+ Returns:
28
+ Formatted title string.
29
+
30
+ Examples:
31
+ >>> format_session_title("worker-1", "cic-3dj", "profile module")
32
+ '[worker-1] cic-3dj: profile module'
33
+
34
+ >>> format_session_title("worker-2", annotation="refactor auth")
35
+ '[worker-2] refactor auth'
36
+
37
+ >>> format_session_title("worker-3")
38
+ '[worker-3]'
39
+ """
40
+ # Build the title in parts
41
+ title_parts = [f"[{session_name}]"]
42
+
43
+ if issue_id and annotation:
44
+ # Both issue ID and annotation: "issue_id: annotation"
45
+ title_parts.append(f"{issue_id}: {annotation}")
46
+ elif issue_id:
47
+ # Only issue ID
48
+ title_parts.append(issue_id)
49
+ elif annotation:
50
+ # Only annotation
51
+ title_parts.append(annotation)
52
+
53
+ return " ".join(title_parts)
54
+
55
+
56
+ def format_badge_text(
57
+ name: str,
58
+ bead: Optional[str] = None,
59
+ annotation: Optional[str] = None,
60
+ agent_type: Optional[str] = None,
61
+ max_annotation_length: int = 30,
62
+ ) -> str:
63
+ """
64
+ Format badge text with bead/name on first line, annotation on second.
65
+
66
+ Creates a multi-line string suitable for iTerm2 badge display:
67
+ - Line 1: Agent type prefix (if not "claude") + bead ID (if provided) or worker name
68
+ - Line 2: annotation (if provided), truncated if too long
69
+
70
+ Args:
71
+ name: Worker name (used if bead not provided)
72
+ bead: Optional bead/issue ID (e.g., "cic-3dj")
73
+ annotation: Optional task annotation
74
+ agent_type: Optional agent type ("claude" or "codex"). If "codex",
75
+ adds a prefix to the first line.
76
+ max_annotation_length: Maximum length for annotation line (default 30)
77
+
78
+ Returns:
79
+ Badge text, potentially multi-line.
80
+
81
+ Examples:
82
+ >>> format_badge_text("Groucho", "cic-3dj", "profile module")
83
+ 'cic-3dj\\nprofile module'
84
+
85
+ >>> format_badge_text("Groucho", annotation="quick task")
86
+ 'Groucho\\nquick task'
87
+
88
+ >>> format_badge_text("Groucho", "cic-3dj")
89
+ 'cic-3dj'
90
+
91
+ >>> format_badge_text("Groucho")
92
+ 'Groucho'
93
+
94
+ >>> format_badge_text("Groucho", annotation="a very long annotation here", max_annotation_length=20)
95
+ 'Groucho\\na very long annot...'
96
+
97
+ >>> format_badge_text("Groucho", agent_type="codex")
98
+ '[Codex] Groucho'
99
+
100
+ >>> format_badge_text("Groucho", "cic-3dj", agent_type="codex")
101
+ '[Codex] cic-3dj'
102
+ """
103
+ # First line: bead if provided, otherwise name
104
+ first_line = bead if bead else name
105
+
106
+ # Add agent type prefix for non-Claude agents
107
+ if agent_type and agent_type != "claude":
108
+ # Capitalize the agent type for display (e.g., "codex" -> "Codex")
109
+ type_display = agent_type.capitalize()
110
+ first_line = f"[{type_display}] {first_line}"
111
+
112
+ # Second line: annotation if provided, with truncation
113
+ if annotation:
114
+ if len(annotation) > max_annotation_length:
115
+ # Reserve 3 chars for ellipsis
116
+ truncated = annotation[: max_annotation_length - 3].rstrip()
117
+ annotation = f"{truncated}..."
118
+ return f"{first_line}\n{annotation}"
119
+
120
+ return first_line