ensemble-claude 0.3.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 (46) hide show
  1. ensemble/__init__.py +5 -0
  2. ensemble/ack.py +86 -0
  3. ensemble/cli.py +31 -0
  4. ensemble/commands/__init__.py +1 -0
  5. ensemble/commands/_init_impl.py +208 -0
  6. ensemble/commands/_launch_impl.py +217 -0
  7. ensemble/commands/init.py +35 -0
  8. ensemble/commands/launch.py +32 -0
  9. ensemble/config.py +218 -0
  10. ensemble/dashboard.py +168 -0
  11. ensemble/helpers.py +79 -0
  12. ensemble/lock.py +77 -0
  13. ensemble/logger.py +80 -0
  14. ensemble/notes.py +221 -0
  15. ensemble/queue.py +166 -0
  16. ensemble/templates/__init__.py +75 -0
  17. ensemble/templates/agents/conductor.md +239 -0
  18. ensemble/templates/agents/dispatch.md +351 -0
  19. ensemble/templates/agents/integrator.md +138 -0
  20. ensemble/templates/agents/learner.md +133 -0
  21. ensemble/templates/agents/reviewer.md +84 -0
  22. ensemble/templates/agents/security-reviewer.md +136 -0
  23. ensemble/templates/agents/worker.md +184 -0
  24. ensemble/templates/commands/go-light.md +49 -0
  25. ensemble/templates/commands/go.md +101 -0
  26. ensemble/templates/commands/improve.md +116 -0
  27. ensemble/templates/commands/review.md +74 -0
  28. ensemble/templates/commands/status.md +56 -0
  29. ensemble/templates/scripts/dashboard-update.sh +78 -0
  30. ensemble/templates/scripts/launch.sh +137 -0
  31. ensemble/templates/scripts/pane-setup.sh +111 -0
  32. ensemble/templates/scripts/setup.sh +163 -0
  33. ensemble/templates/scripts/worktree-create.sh +89 -0
  34. ensemble/templates/scripts/worktree-merge.sh +194 -0
  35. ensemble/templates/workflows/default.yaml +78 -0
  36. ensemble/templates/workflows/heavy.yaml +149 -0
  37. ensemble/templates/workflows/simple.yaml +41 -0
  38. ensemble/templates/workflows/worktree.yaml +202 -0
  39. ensemble/utils.py +60 -0
  40. ensemble/workflow.py +127 -0
  41. ensemble/worktree.py +322 -0
  42. ensemble_claude-0.3.0.dist-info/METADATA +144 -0
  43. ensemble_claude-0.3.0.dist-info/RECORD +46 -0
  44. ensemble_claude-0.3.0.dist-info/WHEEL +4 -0
  45. ensemble_claude-0.3.0.dist-info/entry_points.txt +2 -0
  46. ensemble_claude-0.3.0.dist-info/licenses/LICENSE +21 -0
ensemble/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Ensemble AI Orchestration - Multi-agent orchestration for Claude Code."""
2
+
3
+ __version__ = "0.3.0"
4
+ __author__ = "Ensemble Team"
5
+
ensemble/ack.py ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ ACK(受領確認)機構
3
+
4
+ タスク配信の確認をファイルベースで行う。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+
13
+ from ensemble.lock import atomic_write
14
+
15
+
16
+ class AckManager:
17
+ """
18
+ ACK管理クラス
19
+
20
+ タスク配信後、エージェントからの受領確認を管理する。
21
+ """
22
+
23
+ def __init__(self, ack_dir: Path | None = None) -> None:
24
+ """
25
+ ACKマネージャを初期化する
26
+
27
+ Args:
28
+ ack_dir: ACKファイル保存ディレクトリ(デフォルト: queue/ack/)
29
+ """
30
+ self.ack_dir = ack_dir if ack_dir else Path("queue/ack")
31
+ self.ack_dir.mkdir(parents=True, exist_ok=True)
32
+
33
+ def send(self, task_id: str, agent: str) -> None:
34
+ """
35
+ ACKを送信する
36
+
37
+ Args:
38
+ task_id: タスクID
39
+ agent: ACKを送信したエージェント名
40
+ """
41
+ ack_file = self.ack_dir / f"{task_id}.ack"
42
+ content = f"{agent}\n{datetime.now().isoformat()}\n"
43
+ atomic_write(str(ack_file), content)
44
+
45
+ def wait(self, task_id: str, timeout: float = 30.0, interval: float = 0.1) -> bool:
46
+ """
47
+ ACKを待機する
48
+
49
+ Args:
50
+ task_id: タスクID
51
+ timeout: タイムアウト秒数
52
+ interval: ポーリング間隔
53
+
54
+ Returns:
55
+ ACK受信時True、タイムアウト時False
56
+ """
57
+ ack_file = self.ack_dir / f"{task_id}.ack"
58
+ elapsed = 0.0
59
+
60
+ while elapsed < timeout:
61
+ if ack_file.exists():
62
+ return True
63
+ time.sleep(interval)
64
+ elapsed += interval
65
+
66
+ return False
67
+
68
+ def check(self, task_id: str) -> bool:
69
+ """
70
+ ACKが存在するか確認する
71
+
72
+ Args:
73
+ task_id: タスクID
74
+
75
+ Returns:
76
+ ACK存在時True
77
+ """
78
+ ack_file = self.ack_dir / f"{task_id}.ack"
79
+ return ack_file.exists()
80
+
81
+ def cleanup(self) -> None:
82
+ """
83
+ 全てのACKファイルを削除する
84
+ """
85
+ for ack_file in self.ack_dir.glob("*.ack"):
86
+ ack_file.unlink()
ensemble/cli.py ADDED
@@ -0,0 +1,31 @@
1
+ """Ensemble CLI - AI Orchestration Tool for Claude Code."""
2
+
3
+ import click
4
+
5
+ from ensemble import __version__
6
+ from ensemble.commands.init import init
7
+ from ensemble.commands.launch import launch
8
+
9
+
10
+ @click.group()
11
+ @click.version_option(version=__version__, prog_name="ensemble")
12
+ def cli() -> None:
13
+ """Ensemble - AI Orchestration Tool for Claude Code.
14
+
15
+ Ensemble provides multi-agent orchestration for complex development tasks,
16
+ enabling parallel execution, automatic code review, and self-improvement.
17
+ """
18
+ pass
19
+
20
+
21
+ cli.add_command(init)
22
+ cli.add_command(launch)
23
+
24
+
25
+ def main() -> None:
26
+ """Entry point for the CLI."""
27
+ cli()
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()
@@ -0,0 +1 @@
1
+ """Ensemble CLI commands."""
@@ -0,0 +1,208 @@
1
+ """Implementation of the ensemble init command."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import click
8
+
9
+ from ensemble.templates import get_template_path
10
+
11
+
12
+ def run_init(full: bool = False, force: bool = False) -> None:
13
+ """Run the init command implementation.
14
+
15
+ Args:
16
+ full: If True, copy all agent definitions locally.
17
+ force: If True, overwrite existing files.
18
+ """
19
+ project_root = Path.cwd()
20
+ ensemble_dir = project_root / ".ensemble"
21
+
22
+ click.echo(f"Initializing Ensemble in {project_root}")
23
+
24
+ # Create directory structure
25
+ _create_directories(ensemble_dir)
26
+
27
+ # Create CLAUDE.md or append Ensemble section
28
+ _setup_claude_md(project_root, force)
29
+
30
+ # Update .gitignore
31
+ _update_gitignore(project_root)
32
+
33
+ # Create initial dashboard
34
+ _create_dashboard(ensemble_dir)
35
+
36
+ # Create panes.env placeholder
37
+ _create_panes_env(ensemble_dir)
38
+
39
+ # If --full, copy agent definitions
40
+ if full:
41
+ _copy_agent_definitions(project_root, force)
42
+
43
+ click.echo(click.style("Ensemble initialized successfully!", fg="green"))
44
+ click.echo("\nNext steps:")
45
+ click.echo(" 1. Run 'ensemble launch' to start the tmux session")
46
+ click.echo(" 2. Use '/go <task>' to begin orchestration")
47
+
48
+
49
+ def _create_directories(ensemble_dir: Path) -> None:
50
+ """Create the .ensemble directory structure."""
51
+ dirs = [
52
+ ensemble_dir,
53
+ ensemble_dir / "queue" / "conductor",
54
+ ensemble_dir / "queue" / "tasks",
55
+ ensemble_dir / "queue" / "reports",
56
+ ensemble_dir / "queue" / "ack",
57
+ ensemble_dir / "status",
58
+ ]
59
+
60
+ for dir_path in dirs:
61
+ dir_path.mkdir(parents=True, exist_ok=True)
62
+ click.echo(f" Created {dir_path.relative_to(ensemble_dir.parent)}")
63
+
64
+
65
+ def _setup_claude_md(project_root: Path, force: bool) -> None:
66
+ """Create or update CLAUDE.md with Ensemble section."""
67
+ claude_md = project_root / "CLAUDE.md"
68
+ ensemble_section = '''
69
+ ## Ensemble AI Orchestration
70
+
71
+ This project uses Ensemble for AI-powered development orchestration.
72
+
73
+ ### Quick Start
74
+ - `/go <task>` - Start a task with automatic planning and execution
75
+ - `/go-light <task>` - Lightweight execution for simple changes
76
+ - `/status` - View current progress
77
+
78
+ ### Communication Protocol
79
+ - Agent communication via file-based queue (.ensemble/queue/)
80
+ - Dashboard updates in .ensemble/status/dashboard.md
81
+
82
+ For more information, see the [Ensemble documentation](https://github.com/ChikaKakazu/ensemble).
83
+ '''
84
+
85
+ if claude_md.exists():
86
+ content = claude_md.read_text()
87
+ if "## Ensemble AI Orchestration" in content:
88
+ if force:
89
+ # Remove existing section and re-add
90
+ lines = content.split("\n")
91
+ new_lines = []
92
+ skip = False
93
+ for line in lines:
94
+ if line.startswith("## Ensemble AI Orchestration"):
95
+ skip = True
96
+ continue
97
+ if skip and line.startswith("## "):
98
+ skip = False
99
+ if not skip:
100
+ new_lines.append(line)
101
+ content = "\n".join(new_lines)
102
+ else:
103
+ click.echo(" CLAUDE.md already contains Ensemble section (use --force to overwrite)")
104
+ return
105
+
106
+ # Append Ensemble section
107
+ with open(claude_md, "a") as f:
108
+ f.write(ensemble_section)
109
+ click.echo(" Updated CLAUDE.md with Ensemble section")
110
+ else:
111
+ # Create new CLAUDE.md
112
+ with open(claude_md, "w") as f:
113
+ f.write(f"# {project_root.name}\n")
114
+ f.write(ensemble_section)
115
+ click.echo(" Created CLAUDE.md")
116
+
117
+
118
+ def _update_gitignore(project_root: Path) -> None:
119
+ """Update .gitignore with Ensemble exclusions."""
120
+ gitignore = project_root / ".gitignore"
121
+ ensemble_ignores = """
122
+ # Ensemble
123
+ .ensemble/queue/
124
+ .ensemble/panes.env
125
+ """
126
+
127
+ if gitignore.exists():
128
+ content = gitignore.read_text()
129
+ if "# Ensemble" in content:
130
+ click.echo(" .gitignore already contains Ensemble section")
131
+ return
132
+ with open(gitignore, "a") as f:
133
+ f.write(ensemble_ignores)
134
+ click.echo(" Updated .gitignore")
135
+ else:
136
+ with open(gitignore, "w") as f:
137
+ f.write(ensemble_ignores.strip() + "\n")
138
+ click.echo(" Created .gitignore")
139
+
140
+
141
+ def _create_dashboard(ensemble_dir: Path) -> None:
142
+ """Create initial dashboard.md."""
143
+ dashboard = ensemble_dir / "status" / "dashboard.md"
144
+ content = """# Ensemble Dashboard
145
+
146
+ ## Current Task
147
+ None
148
+
149
+ ## Execution Status
150
+ | Pane/Worktree | Status | Agent | Progress |
151
+ |---|---|---|---|
152
+ | - | idle | - | - |
153
+
154
+ ## Recent Completed Tasks
155
+ | Task | Result | Completed |
156
+ |------|--------|-----------|
157
+ | - | - | - |
158
+
159
+ ---
160
+ *Last updated: -
161
+ """
162
+ dashboard.write_text(content)
163
+ click.echo(" Created status/dashboard.md")
164
+
165
+
166
+ def _create_panes_env(ensemble_dir: Path) -> None:
167
+ """Create panes.env placeholder."""
168
+ panes_env = ensemble_dir / "panes.env"
169
+ content = """# Ensemble pane IDs (auto-generated by launch)
170
+ # This file will be populated when 'ensemble launch' is run
171
+ """
172
+ panes_env.write_text(content)
173
+ click.echo(" Created panes.env placeholder")
174
+
175
+
176
+ def _copy_agent_definitions(project_root: Path, force: bool) -> None:
177
+ """Copy agent definitions to local .claude/agents/."""
178
+ click.echo("\nCopying agent definitions for local customization...")
179
+
180
+ claude_agents_dir = project_root / ".claude" / "agents"
181
+ claude_agents_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ template_agents = get_template_path("agents")
184
+ if not template_agents.exists():
185
+ click.echo(click.style(" Warning: Agent templates not found", fg="yellow"))
186
+ return
187
+
188
+ for agent_file in template_agents.glob("*.md"):
189
+ dest = claude_agents_dir / agent_file.name
190
+ if dest.exists() and not force:
191
+ click.echo(f" Skipped {agent_file.name} (exists, use --force to overwrite)")
192
+ continue
193
+ shutil.copy(agent_file, dest)
194
+ click.echo(f" Copied {agent_file.name}")
195
+
196
+ # Also copy commands
197
+ claude_commands_dir = project_root / ".claude" / "commands"
198
+ claude_commands_dir.mkdir(parents=True, exist_ok=True)
199
+
200
+ template_commands = get_template_path("commands")
201
+ if template_commands.exists():
202
+ for cmd_file in template_commands.glob("*.md"):
203
+ dest = claude_commands_dir / cmd_file.name
204
+ if dest.exists() and not force:
205
+ click.echo(f" Skipped {cmd_file.name} (exists)")
206
+ continue
207
+ shutil.copy(cmd_file, dest)
208
+ click.echo(f" Copied {cmd_file.name}")
@@ -0,0 +1,217 @@
1
+ """Implementation of the ensemble launch command."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import click
11
+
12
+ from ensemble.templates import get_template_path
13
+
14
+
15
+ def run_launch(session: str = "ensemble", attach: bool = True) -> None:
16
+ """Run the launch command implementation.
17
+
18
+ Args:
19
+ session: Name of the tmux session.
20
+ attach: Whether to attach to the session after creation.
21
+ """
22
+ # Check prerequisites
23
+ if not _check_tmux():
24
+ click.echo(click.style("Error: tmux is not installed or not in PATH", fg="red"))
25
+ sys.exit(1)
26
+
27
+ if not _check_claude():
28
+ click.echo(click.style("Error: claude CLI is not installed or not in PATH", fg="red"))
29
+ click.echo("Install Claude Code from: https://claude.ai/code")
30
+ sys.exit(1)
31
+
32
+ # Check if session already exists
33
+ if _session_exists(session):
34
+ click.echo(f"Session '{session}' already exists.")
35
+ if attach:
36
+ click.echo(f"Attaching to existing session...")
37
+ _attach_session(session)
38
+ return
39
+
40
+ project_root = Path.cwd()
41
+ ensemble_dir = project_root / ".ensemble"
42
+
43
+ # Verify project is initialized
44
+ if not ensemble_dir.exists():
45
+ click.echo(click.style("Error: Project not initialized for Ensemble", fg="red"))
46
+ click.echo("Run 'ensemble init' first")
47
+ sys.exit(1)
48
+
49
+ click.echo(f"Launching Ensemble session '{session}'...")
50
+
51
+ # Get agent paths
52
+ agents = _resolve_agent_paths(project_root)
53
+
54
+ # Create tmux session with layout
55
+ _create_session(session, project_root, agents)
56
+
57
+ # Save pane IDs
58
+ _save_pane_ids(session, ensemble_dir)
59
+
60
+ click.echo(click.style("Ensemble session started!", fg="green"))
61
+
62
+ if attach:
63
+ _attach_session(session)
64
+
65
+
66
+ def _check_tmux() -> bool:
67
+ """Check if tmux is available."""
68
+ return shutil.which("tmux") is not None
69
+
70
+
71
+ def _check_claude() -> bool:
72
+ """Check if claude CLI is available."""
73
+ return shutil.which("claude") is not None
74
+
75
+
76
+ def _session_exists(session: str) -> bool:
77
+ """Check if a tmux session exists."""
78
+ result = subprocess.run(
79
+ ["tmux", "has-session", "-t", session],
80
+ capture_output=True,
81
+ )
82
+ return result.returncode == 0
83
+
84
+
85
+ def _attach_session(session: str) -> None:
86
+ """Attach to an existing tmux session."""
87
+ os.execvp("tmux", ["tmux", "attach-session", "-t", session])
88
+
89
+
90
+ def _resolve_agent_paths(project_root: Path) -> dict[str, Path]:
91
+ """Resolve agent definition paths with priority.
92
+
93
+ Priority order:
94
+ 1. Local project: ./.claude/agents/
95
+ 2. Global config: ~/.config/ensemble/agents/
96
+ 3. Package templates
97
+ """
98
+ agents = {}
99
+ agent_names = ["conductor", "dispatch", "worker", "reviewer", "security-reviewer", "integrator", "learner"]
100
+
101
+ local_agents = project_root / ".claude" / "agents"
102
+ global_agents = Path.home() / ".config" / "ensemble" / "agents"
103
+ template_agents = get_template_path("agents")
104
+
105
+ for name in agent_names:
106
+ filename = f"{name}.md"
107
+
108
+ # Check local first
109
+ local_path = local_agents / filename
110
+ if local_path.exists():
111
+ agents[name] = local_path
112
+ continue
113
+
114
+ # Check global
115
+ global_path = global_agents / filename
116
+ if global_path.exists():
117
+ agents[name] = global_path
118
+ continue
119
+
120
+ # Fall back to template
121
+ template_path = template_agents / filename
122
+ if template_path.exists():
123
+ agents[name] = template_path
124
+
125
+ return agents
126
+
127
+
128
+ def _create_session(session: str, project_root: Path, agents: dict[str, Path]) -> None:
129
+ """Create the tmux session with Ensemble layout."""
130
+ # Create new session (detached)
131
+ subprocess.run(
132
+ [
133
+ "tmux", "new-session",
134
+ "-d",
135
+ "-s", session,
136
+ "-c", str(project_root),
137
+ "-n", "main",
138
+ ],
139
+ check=True,
140
+ )
141
+
142
+ # Split into 3 panes: Conductor (left), Dashboard (top-right), Dispatch (bottom-right)
143
+ # First split vertically (creates pane 1 on right)
144
+ subprocess.run(
145
+ ["tmux", "split-window", "-t", f"{session}:main", "-h", "-c", str(project_root)],
146
+ check=True,
147
+ )
148
+
149
+ # Split the right pane horizontally (creates pane 2 at bottom-right)
150
+ subprocess.run(
151
+ ["tmux", "split-window", "-t", f"{session}:main.1", "-v", "-c", str(project_root)],
152
+ check=True,
153
+ )
154
+
155
+ # Set pane titles
156
+ subprocess.run(["tmux", "select-pane", "-t", f"{session}:main.0", "-T", "conductor"], check=True)
157
+ subprocess.run(["tmux", "select-pane", "-t", f"{session}:main.1", "-T", "dashboard"], check=True)
158
+ subprocess.run(["tmux", "select-pane", "-t", f"{session}:main.2", "-T", "dispatch"], check=True)
159
+
160
+ # Start Claude in Conductor pane (pane 0)
161
+ conductor_agent = agents.get("conductor")
162
+ if conductor_agent:
163
+ cmd = f"claude --agent {conductor_agent}"
164
+ else:
165
+ cmd = "claude"
166
+ subprocess.run(
167
+ ["tmux", "send-keys", "-t", f"{session}:main.0", cmd, "Enter"],
168
+ check=True,
169
+ )
170
+
171
+ # Start dashboard watch in Dashboard pane (pane 1)
172
+ dashboard_path = project_root / ".ensemble" / "status" / "dashboard.md"
173
+ subprocess.run(
174
+ ["tmux", "send-keys", "-t", f"{session}:main.1", f"watch -n 2 cat {dashboard_path}", "Enter"],
175
+ check=True,
176
+ )
177
+
178
+ # Start Claude in Dispatch pane (pane 2)
179
+ dispatch_agent = agents.get("dispatch")
180
+ if dispatch_agent:
181
+ cmd = f"claude --agent {dispatch_agent}"
182
+ else:
183
+ cmd = "claude"
184
+ subprocess.run(
185
+ ["tmux", "send-keys", "-t", f"{session}:main.2", cmd, "Enter"],
186
+ check=True,
187
+ )
188
+
189
+ # Select Conductor pane
190
+ subprocess.run(["tmux", "select-pane", "-t", f"{session}:main.0"], check=True)
191
+
192
+
193
+ def _save_pane_ids(session: str, ensemble_dir: Path) -> None:
194
+ """Save pane IDs to panes.env file."""
195
+ # Get pane IDs
196
+ result = subprocess.run(
197
+ ["tmux", "list-panes", "-t", f"{session}:main", "-F", "#{pane_index}:#{pane_id}"],
198
+ capture_output=True,
199
+ text=True,
200
+ check=True,
201
+ )
202
+
203
+ pane_map = {}
204
+ for line in result.stdout.strip().split("\n"):
205
+ if ":" in line:
206
+ idx, pane_id = line.split(":", 1)
207
+ pane_map[int(idx)] = pane_id
208
+
209
+ # Write panes.env
210
+ panes_env = ensemble_dir / "panes.env"
211
+ with open(panes_env, "w") as f:
212
+ f.write("# Ensemble pane IDs (auto-generated)\n")
213
+ f.write(f"CONDUCTOR_PANE={pane_map.get(0, '%0')}\n")
214
+ f.write(f"DASHBOARD_PANE={pane_map.get(1, '%1')}\n")
215
+ f.write(f"DISPATCH_PANE={pane_map.get(2, '%2')}\n")
216
+
217
+ click.echo(f" Saved pane IDs to {panes_env.relative_to(ensemble_dir.parent)}")
@@ -0,0 +1,35 @@
1
+ """Ensemble init command - Initialize a project for Ensemble."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.option(
10
+ "--full",
11
+ is_flag=True,
12
+ help="Copy all agent definitions to local project for customization.",
13
+ )
14
+ @click.option(
15
+ "--force",
16
+ is_flag=True,
17
+ help="Overwrite existing configuration files.",
18
+ )
19
+ @click.pass_context
20
+ def init(ctx: click.Context, full: bool, force: bool) -> None:
21
+ """Initialize the current project for Ensemble.
22
+
23
+ This command sets up the necessary directory structure and configuration
24
+ files for using Ensemble in your project.
25
+
26
+ Creates:
27
+ - .ensemble/ directory with queue/ and status/ subdirectories
28
+ - Adds Ensemble section to CLAUDE.md (creates if not exists)
29
+ - Updates .gitignore to exclude queue files
30
+
31
+ Use --full to copy all agent definitions locally for customization.
32
+ """
33
+ from ensemble.commands._init_impl import run_init
34
+
35
+ run_init(full=full, force=force)
@@ -0,0 +1,32 @@
1
+ """Ensemble launch command - Start the Ensemble tmux session."""
2
+
3
+ import click
4
+
5
+
6
+ @click.command()
7
+ @click.option(
8
+ "--session",
9
+ "-s",
10
+ default="ensemble",
11
+ help="Name of the tmux session to create.",
12
+ )
13
+ @click.option(
14
+ "--attach/--no-attach",
15
+ default=True,
16
+ help="Attach to the session after creation.",
17
+ )
18
+ @click.pass_context
19
+ def launch(ctx: click.Context, session: str, attach: bool) -> None:
20
+ """Launch the Ensemble tmux session.
21
+
22
+ This command creates a tmux session with the Conductor, Dispatch,
23
+ and Dashboard panes configured and ready for orchestration.
24
+
25
+ The session will use configuration from (in priority order):
26
+ 1. ./.ensemble/agents/*.md (local project customization)
27
+ 2. ~/.config/ensemble/agents/*.md (global customization)
28
+ 3. Package bundled templates (defaults)
29
+ """
30
+ from ensemble.commands._launch_impl import run_launch
31
+
32
+ run_launch(session=session, attach=attach)