ensemble-claude 0.3.1__tar.gz → 0.4.1__tar.gz

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 (72) hide show
  1. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/.gitignore +3 -0
  2. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/PKG-INFO +8 -5
  3. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/README.md +7 -4
  4. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/pyproject.toml +1 -1
  5. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/__init__.py +1 -1
  6. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/cli.py +2 -0
  7. ensemble_claude-0.4.1/src/ensemble/commands/_issue_impl.py +150 -0
  8. ensemble_claude-0.4.1/src/ensemble/commands/_launch_impl.py +342 -0
  9. ensemble_claude-0.4.1/src/ensemble/commands/issue.py +36 -0
  10. ensemble_claude-0.4.1/src/ensemble/git_utils.py +157 -0
  11. ensemble_claude-0.4.1/src/ensemble/issue_provider.py +97 -0
  12. ensemble_claude-0.4.1/src/ensemble/providers/__init__.py +5 -0
  13. ensemble_claude-0.4.1/src/ensemble/providers/github.py +103 -0
  14. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/conductor.md +64 -10
  15. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/dispatch.md +128 -14
  16. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/worker.md +55 -0
  17. ensemble_claude-0.4.1/src/ensemble/templates/commands/go-issue.md +157 -0
  18. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/go.md +19 -4
  19. ensemble_claude-0.4.1/src/ensemble/templates/scripts/launch.sh +176 -0
  20. ensemble_claude-0.4.1/src/ensemble/templates/scripts/pane-setup.sh +184 -0
  21. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_cli.py +7 -2
  22. ensemble_claude-0.4.1/tests/test_git_utils.py +232 -0
  23. ensemble_claude-0.4.1/tests/test_issue_command.py +137 -0
  24. ensemble_claude-0.4.1/tests/test_issue_provider.py +265 -0
  25. ensemble_claude-0.3.1/src/ensemble/commands/_launch_impl.py +0 -217
  26. ensemble_claude-0.3.1/src/ensemble/templates/scripts/launch.sh +0 -137
  27. ensemble_claude-0.3.1/src/ensemble/templates/scripts/pane-setup.sh +0 -111
  28. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/LICENSE +0 -0
  29. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/ack.py +0 -0
  30. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/__init__.py +0 -0
  31. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/_init_impl.py +0 -0
  32. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/init.py +0 -0
  33. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/launch.py +0 -0
  34. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/config.py +0 -0
  35. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/dashboard.py +0 -0
  36. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/helpers.py +0 -0
  37. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/lock.py +0 -0
  38. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/logger.py +0 -0
  39. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/notes.py +0 -0
  40. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/queue.py +0 -0
  41. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/__init__.py +0 -0
  42. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/integrator.md +0 -0
  43. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/learner.md +0 -0
  44. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/reviewer.md +0 -0
  45. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/security-reviewer.md +0 -0
  46. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/go-light.md +0 -0
  47. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/improve.md +0 -0
  48. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/review.md +0 -0
  49. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/status.md +0 -0
  50. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/dashboard-update.sh +0 -0
  51. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/setup.sh +0 -0
  52. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/worktree-create.sh +0 -0
  53. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/worktree-merge.sh +0 -0
  54. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/default.yaml +0 -0
  55. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/heavy.yaml +0 -0
  56. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/simple.yaml +0 -0
  57. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/worktree.yaml +0 -0
  58. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/utils.py +0 -0
  59. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/workflow.py +0 -0
  60. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/worktree.py +0 -0
  61. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/__init__.py +0 -0
  62. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_ack.py +0 -0
  63. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_config.py +0 -0
  64. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_dashboard.py +0 -0
  65. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_lock.py +0 -0
  66. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_logger.py +0 -0
  67. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_notes.py +0 -0
  68. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_queue.py +0 -0
  69. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_setup.py +0 -0
  70. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_templates.py +0 -0
  71. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_workflow.py +0 -0
  72. {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_worktree.py +0 -0
@@ -1,3 +1,6 @@
1
+ # Ensemble working directory
2
+ .ensemble/
3
+
1
4
  # Ensemble queue files (transient)
2
5
  queue/tasks/*.yaml
3
6
  queue/reports/*.yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ensemble-claude
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: AI Orchestration Tool for Claude Code - Multi-agent orchestration for complex development tasks
5
5
  Project-URL: Homepage, https://github.com/ChikaKakazu/ensemble
6
6
  Project-URL: Documentation, https://github.com/ChikaKakazu/ensemble#readme
@@ -87,10 +87,13 @@ pip install -e .
87
87
  # 1. Initialize Ensemble in your project
88
88
  ensemble init
89
89
 
90
- # 2. Launch the tmux session with Conductor + Dispatch
90
+ # 2. Launch the tmux sessions (2 separate sessions)
91
91
  ensemble launch
92
92
 
93
- # 3. Run a task (in the Conductor pane)
93
+ # 3. Open another terminal to view workers session
94
+ tmux attach -t ensemble-workers
95
+
96
+ # 4. Run a task in the Conductor session
94
97
  /go implement user authentication
95
98
 
96
99
  # Light workflow (minimal cost)
@@ -103,8 +106,8 @@ ensemble launch
103
106
  |---------|-------------|
104
107
  | `ensemble init` | Initialize Ensemble in current project |
105
108
  | `ensemble init --full` | Also copy agent/command definitions locally |
106
- | `ensemble launch` | Start tmux session with Conductor + Dispatch |
107
- | `ensemble launch --no-attach` | Start session without attaching |
109
+ | `ensemble launch` | Start 2 tmux sessions (conductor + workers) |
110
+ | `ensemble launch --no-attach` | Start sessions without attaching |
108
111
  | `ensemble --version` | Show version |
109
112
 
110
113
  ## Requirements
@@ -59,10 +59,13 @@ pip install -e .
59
59
  # 1. Initialize Ensemble in your project
60
60
  ensemble init
61
61
 
62
- # 2. Launch the tmux session with Conductor + Dispatch
62
+ # 2. Launch the tmux sessions (2 separate sessions)
63
63
  ensemble launch
64
64
 
65
- # 3. Run a task (in the Conductor pane)
65
+ # 3. Open another terminal to view workers session
66
+ tmux attach -t ensemble-workers
67
+
68
+ # 4. Run a task in the Conductor session
66
69
  /go implement user authentication
67
70
 
68
71
  # Light workflow (minimal cost)
@@ -75,8 +78,8 @@ ensemble launch
75
78
  |---------|-------------|
76
79
  | `ensemble init` | Initialize Ensemble in current project |
77
80
  | `ensemble init --full` | Also copy agent/command definitions locally |
78
- | `ensemble launch` | Start tmux session with Conductor + Dispatch |
79
- | `ensemble launch --no-attach` | Start session without attaching |
81
+ | `ensemble launch` | Start 2 tmux sessions (conductor + workers) |
82
+ | `ensemble launch --no-attach` | Start sessions without attaching |
80
83
  | `ensemble --version` | Show version |
81
84
 
82
85
  ## Requirements
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ensemble-claude"
3
- version = "0.3.1"
3
+ version = "0.4.1"
4
4
  description = "AI Orchestration Tool for Claude Code - Multi-agent orchestration for complex development tasks"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,5 +1,5 @@
1
1
  """Ensemble AI Orchestration - Multi-agent orchestration for Claude Code."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.4.1"
4
4
  __author__ = "Ensemble Team"
5
5
 
@@ -4,6 +4,7 @@ import click
4
4
 
5
5
  from ensemble import __version__
6
6
  from ensemble.commands.init import init
7
+ from ensemble.commands.issue import issue
7
8
  from ensemble.commands.launch import launch
8
9
 
9
10
 
@@ -19,6 +20,7 @@ def cli() -> None:
19
20
 
20
21
 
21
22
  cli.add_command(init)
23
+ cli.add_command(issue)
22
24
  cli.add_command(launch)
23
25
 
24
26
 
@@ -0,0 +1,150 @@
1
+ """Implementation of the ensemble issue command."""
2
+
3
+ import click
4
+
5
+ from ensemble.issue_provider import Issue, IssueProvider
6
+ from ensemble.providers.github import GitHubProvider
7
+
8
+
9
+ def get_provider(provider_name: str) -> IssueProvider:
10
+ """Get the issue provider by name.
11
+
12
+ Args:
13
+ provider_name: Name of the provider ('github', 'gitlab').
14
+
15
+ Returns:
16
+ IssueProvider instance.
17
+
18
+ Raises:
19
+ click.ClickException: If provider is not available.
20
+ """
21
+ if provider_name == "github":
22
+ provider = GitHubProvider()
23
+ if not provider.is_available():
24
+ raise click.ClickException(
25
+ "GitHub CLI (gh) is not installed. "
26
+ "Install it from: https://cli.github.com/"
27
+ )
28
+ return provider
29
+ elif provider_name == "gitlab":
30
+ raise click.ClickException(
31
+ "GitLab provider is not yet implemented. Coming soon!"
32
+ )
33
+ else:
34
+ raise click.ClickException(f"Unknown provider: {provider_name}")
35
+
36
+
37
+ def run_issue(
38
+ identifier: str | None,
39
+ state: str,
40
+ provider: str,
41
+ ) -> None:
42
+ """Run the issue command implementation.
43
+
44
+ Args:
45
+ identifier: Issue number or URL (None to list issues).
46
+ state: Filter state for listing ('open', 'closed', 'all').
47
+ provider: Provider name ('github', 'gitlab').
48
+ """
49
+ issue_provider = get_provider(provider)
50
+
51
+ if identifier:
52
+ # View specific issue
53
+ _view_issue(issue_provider, identifier)
54
+ else:
55
+ # List issues
56
+ _list_issues(issue_provider, state)
57
+
58
+
59
+ def _view_issue(provider: IssueProvider, identifier: str) -> None:
60
+ """View a specific issue.
61
+
62
+ Args:
63
+ provider: Issue provider instance.
64
+ identifier: Issue number or URL.
65
+ """
66
+ try:
67
+ issue = provider.get_issue(identifier)
68
+ _print_issue_detail(issue)
69
+ except ValueError as e:
70
+ raise click.ClickException(str(e))
71
+ except RuntimeError as e:
72
+ raise click.ClickException(str(e))
73
+
74
+
75
+ def _list_issues(provider: IssueProvider, state: str) -> None:
76
+ """List issues.
77
+
78
+ Args:
79
+ provider: Issue provider instance.
80
+ state: Filter state.
81
+ """
82
+ try:
83
+ issues = provider.list_issues(state=state)
84
+
85
+ if not issues:
86
+ click.echo(f"No {state} issues found.")
87
+ return
88
+
89
+ click.echo(f"\n{click.style(f'{len(issues)} {state} issue(s):', bold=True)}\n")
90
+
91
+ for issue in issues:
92
+ _print_issue_summary(issue)
93
+
94
+ click.echo("")
95
+
96
+ except RuntimeError as e:
97
+ raise click.ClickException(str(e))
98
+
99
+
100
+ def _print_issue_summary(issue: Issue) -> None:
101
+ """Print a one-line summary of an issue.
102
+
103
+ Args:
104
+ issue: Issue to print.
105
+ """
106
+ # Format: #123 [bug, urgent] Issue title
107
+ number = click.style(f"#{issue.number}", fg="cyan", bold=True)
108
+ labels = ""
109
+ if issue.labels:
110
+ label_str = ", ".join(issue.labels[:3]) # Max 3 labels
111
+ if len(issue.labels) > 3:
112
+ label_str += ", ..."
113
+ labels = click.style(f" [{label_str}]", fg="yellow")
114
+
115
+ # Truncate title if too long
116
+ title = issue.title
117
+ if len(title) > 60:
118
+ title = title[:57] + "..."
119
+
120
+ click.echo(f" {number}{labels} {title}")
121
+
122
+
123
+ def _print_issue_detail(issue: Issue) -> None:
124
+ """Print detailed view of an issue.
125
+
126
+ Args:
127
+ issue: Issue to print.
128
+ """
129
+ click.echo("")
130
+ click.echo(click.style(f"Issue #{issue.number}", fg="cyan", bold=True))
131
+ click.echo(click.style("=" * 60, dim=True))
132
+ click.echo("")
133
+ click.echo(f"{click.style('Title:', bold=True)} {issue.title}")
134
+ click.echo(f"{click.style('State:', bold=True)} {issue.state}")
135
+
136
+ if issue.labels:
137
+ labels = ", ".join(issue.labels)
138
+ click.echo(f"{click.style('Labels:', bold=True)} {labels}")
139
+
140
+ click.echo(f"{click.style('URL:', bold=True)} {issue.url}")
141
+ click.echo("")
142
+
143
+ if issue.body:
144
+ click.echo(click.style("Description:", bold=True))
145
+ click.echo(click.style("-" * 40, dim=True))
146
+ click.echo(issue.body)
147
+ else:
148
+ click.echo(click.style("(No description)", dim=True))
149
+
150
+ click.echo("")
@@ -0,0 +1,342 @@
1
+ """Implementation of the ensemble launch command."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import click
12
+
13
+ from ensemble.templates import get_template_path
14
+
15
+
16
+ def run_launch(session: str = "ensemble", attach: bool = True) -> None:
17
+ """Run the launch command implementation.
18
+
19
+ Args:
20
+ session: Base name for the tmux sessions (will create {session}-conductor and {session}-workers).
21
+ attach: Whether to attach to the session after creation.
22
+ """
23
+ # Check prerequisites
24
+ if not _check_tmux():
25
+ click.echo(click.style("Error: tmux is not installed or not in PATH", fg="red"))
26
+ sys.exit(1)
27
+
28
+ if not _check_claude():
29
+ click.echo(click.style("Error: claude CLI is not installed or not in PATH", fg="red"))
30
+ click.echo("Install Claude Code from: https://claude.ai/code")
31
+ sys.exit(1)
32
+
33
+ # Session names
34
+ conductor_session = f"{session}-conductor"
35
+ workers_session = f"{session}-workers"
36
+
37
+ # Check if sessions already exist
38
+ conductor_exists = _session_exists(conductor_session)
39
+ workers_exists = _session_exists(workers_session)
40
+
41
+ if conductor_exists or workers_exists:
42
+ click.echo("Existing Ensemble sessions found:")
43
+ if conductor_exists:
44
+ click.echo(f" - {conductor_session}")
45
+ if workers_exists:
46
+ click.echo(f" - {workers_session}")
47
+ click.echo("")
48
+ click.echo("To attach, open two terminal windows and run:")
49
+ click.echo(f" Terminal 1: tmux attach -t {conductor_session}")
50
+ click.echo(f" Terminal 2: tmux attach -t {workers_session}")
51
+ return
52
+
53
+ project_root = Path.cwd()
54
+ ensemble_dir = project_root / ".ensemble"
55
+
56
+ # Verify project is initialized
57
+ if not ensemble_dir.exists():
58
+ click.echo(click.style("Error: Project not initialized for Ensemble", fg="red"))
59
+ click.echo("Run 'ensemble init' first")
60
+ sys.exit(1)
61
+
62
+ click.echo(f"Launching Ensemble sessions '{session}-*'...")
63
+
64
+ # Clean up queue directories
65
+ queue_dir = project_root / "queue"
66
+ if queue_dir.exists():
67
+ for subdir in ["tasks", "processing", "reports", "ack"]:
68
+ subdir_path = queue_dir / subdir
69
+ if subdir_path.exists():
70
+ for f in subdir_path.glob("*.yaml"):
71
+ f.unlink()
72
+ for f in subdir_path.glob("*.ack"):
73
+ f.unlink()
74
+
75
+ # Ensure queue directories exist
76
+ for subdir in ["tasks", "processing", "reports", "ack", "conductor"]:
77
+ (queue_dir / subdir).mkdir(parents=True, exist_ok=True)
78
+
79
+ # Get agent paths
80
+ agents = _resolve_agent_paths(project_root)
81
+
82
+ # Create tmux sessions (2 separate sessions)
83
+ _create_sessions(session, project_root, agents)
84
+
85
+ # Save pane IDs
86
+ _save_pane_ids(session, ensemble_dir)
87
+
88
+ click.echo(click.style("Ensemble sessions started!", fg="green"))
89
+ click.echo("")
90
+ click.echo("==========================================")
91
+ click.echo(" Two separate tmux sessions created!")
92
+ click.echo("==========================================")
93
+ click.echo("")
94
+ click.echo(f"Session 1: {conductor_session}")
95
+ click.echo(" +------------------+------------------+")
96
+ click.echo(" | Conductor | dashboard |")
97
+ click.echo(" +------------------+------------------+")
98
+ click.echo("")
99
+ click.echo(f"Session 2: {workers_session}")
100
+ click.echo(" +------------------+------------------+")
101
+ click.echo(" | dispatch | worker-area |")
102
+ click.echo(" +------------------+------------------+")
103
+ click.echo("")
104
+ click.echo("To view both simultaneously, open two terminal windows:")
105
+ click.echo(f" Terminal 1: tmux attach -t {conductor_session}")
106
+ click.echo(f" Terminal 2: tmux attach -t {workers_session}")
107
+ click.echo("")
108
+
109
+ if attach:
110
+ click.echo(f"Attaching to {conductor_session}...")
111
+ click.echo(f"(Open another terminal and run: tmux attach -t {workers_session})")
112
+ click.echo("")
113
+ _attach_session(conductor_session)
114
+
115
+
116
+ def _check_tmux() -> bool:
117
+ """Check if tmux is available."""
118
+ return shutil.which("tmux") is not None
119
+
120
+
121
+ def _check_claude() -> bool:
122
+ """Check if claude CLI is available."""
123
+ return shutil.which("claude") is not None
124
+
125
+
126
+ def _session_exists(session: str) -> bool:
127
+ """Check if a tmux session exists."""
128
+ result = subprocess.run(
129
+ ["tmux", "has-session", "-t", session],
130
+ capture_output=True,
131
+ )
132
+ return result.returncode == 0
133
+
134
+
135
+ def _attach_session(session: str) -> None:
136
+ """Attach to an existing tmux session."""
137
+ os.execvp("tmux", ["tmux", "attach-session", "-t", session])
138
+
139
+
140
+ def _resolve_agent_paths(project_root: Path) -> dict[str, Path]:
141
+ """Resolve agent definition paths with priority.
142
+
143
+ Priority order:
144
+ 1. Local project: ./.claude/agents/
145
+ 2. Global config: ~/.config/ensemble/agents/
146
+ 3. Package templates
147
+ """
148
+ agents = {}
149
+ agent_names = ["conductor", "dispatch", "worker", "reviewer", "security-reviewer", "integrator", "learner"]
150
+
151
+ local_agents = project_root / ".claude" / "agents"
152
+ global_agents = Path.home() / ".config" / "ensemble" / "agents"
153
+ template_agents = get_template_path("agents")
154
+
155
+ for name in agent_names:
156
+ filename = f"{name}.md"
157
+
158
+ # Check local first
159
+ local_path = local_agents / filename
160
+ if local_path.exists():
161
+ agents[name] = local_path
162
+ continue
163
+
164
+ # Check global
165
+ global_path = global_agents / filename
166
+ if global_path.exists():
167
+ agents[name] = global_path
168
+ continue
169
+
170
+ # Fall back to template
171
+ template_path = template_agents / filename
172
+ if template_path.exists():
173
+ agents[name] = template_path
174
+
175
+ return agents
176
+
177
+
178
+ def _create_sessions(session: str, project_root: Path, agents: dict[str, Path]) -> None:
179
+ """Create two separate tmux sessions for Ensemble.
180
+
181
+ Session 1 ({session}-conductor): Conductor (left 60%) + Dashboard (right 40%)
182
+ Session 2 ({session}-workers): Dispatch (left 60%) + Worker area (right 40%)
183
+
184
+ This allows viewing both sessions simultaneously in separate terminal windows.
185
+ """
186
+ conductor_session = f"{session}-conductor"
187
+ workers_session = f"{session}-workers"
188
+
189
+ # === Session 1: Conductor ===
190
+ subprocess.run(
191
+ [
192
+ "tmux", "new-session",
193
+ "-d",
194
+ "-s", conductor_session,
195
+ "-c", str(project_root),
196
+ "-n", "main",
197
+ ],
198
+ check=True,
199
+ )
200
+
201
+ # Split conductor window: left/right (60/40)
202
+ subprocess.run(
203
+ ["tmux", "split-window", "-t", f"{conductor_session}:main", "-h", "-l", "40%", "-c", str(project_root)],
204
+ check=True,
205
+ )
206
+
207
+ # Set pane titles
208
+ subprocess.run(["tmux", "select-pane", "-t", f"{conductor_session}:main.0", "-T", "conductor"], check=True)
209
+ subprocess.run(["tmux", "select-pane", "-t", f"{conductor_session}:main.1", "-T", "dashboard"], check=True)
210
+
211
+ # Start Claude in Conductor pane (left)
212
+ conductor_agent = agents.get("conductor")
213
+ if conductor_agent:
214
+ cmd = f"MAX_THINKING_TOKENS=0 claude --agent {conductor_agent} --model opus --dangerously-skip-permissions"
215
+ else:
216
+ cmd = "MAX_THINKING_TOKENS=0 claude --model opus --dangerously-skip-permissions"
217
+ subprocess.run(
218
+ ["tmux", "send-keys", "-t", f"{conductor_session}:main.0", cmd],
219
+ check=True,
220
+ )
221
+ time.sleep(1)
222
+ subprocess.run(
223
+ ["tmux", "send-keys", "-t", f"{conductor_session}:main.0", "Enter"],
224
+ check=True,
225
+ )
226
+
227
+ # Start dashboard watch in Dashboard pane (right)
228
+ dashboard_path = project_root / ".ensemble" / "status" / "dashboard.md"
229
+ subprocess.run(
230
+ ["tmux", "send-keys", "-t", f"{conductor_session}:main.1", f"watch -n 5 cat {dashboard_path}"],
231
+ check=True,
232
+ )
233
+ time.sleep(1)
234
+ subprocess.run(
235
+ ["tmux", "send-keys", "-t", f"{conductor_session}:main.1", "Enter"],
236
+ check=True,
237
+ )
238
+
239
+ # フレンドリーファイア防止: Conductorが起動完了するまで待機
240
+ time.sleep(3)
241
+
242
+ # === Session 2: Workers ===
243
+ subprocess.run(
244
+ [
245
+ "tmux", "new-session",
246
+ "-d",
247
+ "-s", workers_session,
248
+ "-c", str(project_root),
249
+ "-n", "main",
250
+ ],
251
+ check=True,
252
+ )
253
+
254
+ # Split workers window: left/right (60/40)
255
+ subprocess.run(
256
+ ["tmux", "split-window", "-t", f"{workers_session}:main", "-h", "-l", "40%", "-c", str(project_root)],
257
+ check=True,
258
+ )
259
+
260
+ # Set pane titles
261
+ subprocess.run(["tmux", "select-pane", "-t", f"{workers_session}:main.0", "-T", "dispatch"], check=True)
262
+ subprocess.run(["tmux", "select-pane", "-t", f"{workers_session}:main.1", "-T", "worker-area"], check=True)
263
+
264
+ # Start Claude in Dispatch pane (left)
265
+ dispatch_agent = agents.get("dispatch")
266
+ if dispatch_agent:
267
+ cmd = f"claude --agent {dispatch_agent} --model sonnet --dangerously-skip-permissions"
268
+ else:
269
+ cmd = "claude --model sonnet --dangerously-skip-permissions"
270
+ subprocess.run(
271
+ ["tmux", "send-keys", "-t", f"{workers_session}:main.0", cmd],
272
+ check=True,
273
+ )
274
+ time.sleep(1)
275
+ subprocess.run(
276
+ ["tmux", "send-keys", "-t", f"{workers_session}:main.0", "Enter"],
277
+ check=True,
278
+ )
279
+
280
+ # Show placeholder message in worker area (right)
281
+ subprocess.run(
282
+ ["tmux", "send-keys", "-t", f"{workers_session}:main.1", "echo '=== Worker Area ===' && echo 'Workers will be started here.'", "Enter"],
283
+ check=True,
284
+ )
285
+
286
+ # Select dispatch pane in workers session
287
+ subprocess.run(["tmux", "select-pane", "-t", f"{workers_session}:main.0"], check=True)
288
+
289
+
290
+ def _save_pane_ids(session: str, ensemble_dir: Path) -> None:
291
+ """Save pane IDs to panes.env file."""
292
+ conductor_session = f"{session}-conductor"
293
+ workers_session = f"{session}-workers"
294
+
295
+ # Get conductor session pane IDs
296
+ result = subprocess.run(
297
+ ["tmux", "list-panes", "-t", f"{conductor_session}:main", "-F", "#{pane_index}:#{pane_id}"],
298
+ capture_output=True,
299
+ text=True,
300
+ check=True,
301
+ )
302
+
303
+ conductor_pane_map = {}
304
+ for line in result.stdout.strip().split("\n"):
305
+ if ":" in line:
306
+ idx, pane_id = line.split(":", 1)
307
+ conductor_pane_map[int(idx)] = pane_id
308
+
309
+ # Get workers session pane IDs
310
+ result = subprocess.run(
311
+ ["tmux", "list-panes", "-t", f"{workers_session}:main", "-F", "#{pane_index}:#{pane_id}"],
312
+ capture_output=True,
313
+ text=True,
314
+ check=True,
315
+ )
316
+
317
+ workers_pane_map = {}
318
+ for line in result.stdout.strip().split("\n"):
319
+ if ":" in line:
320
+ idx, pane_id = line.split(":", 1)
321
+ workers_pane_map[int(idx)] = pane_id
322
+
323
+ # Write panes.env
324
+ panes_env = ensemble_dir / "panes.env"
325
+ with open(panes_env, "w") as f:
326
+ f.write("# Ensemble pane IDs (auto-generated)\n")
327
+ f.write("# Session names\n")
328
+ f.write(f"CONDUCTOR_SESSION={conductor_session}\n")
329
+ f.write(f"WORKERS_SESSION={workers_session}\n")
330
+ f.write("\n")
331
+ f.write("# Pane IDs (use these with tmux send-keys -t)\n")
332
+ f.write(f"CONDUCTOR_PANE={conductor_pane_map.get(0, '%0')}\n")
333
+ f.write(f"DASHBOARD_PANE={conductor_pane_map.get(1, '%1')}\n")
334
+ f.write(f"DISPATCH_PANE={workers_pane_map.get(0, '%0')}\n")
335
+ f.write(f"WORKER_AREA_PANE={workers_pane_map.get(1, '%1')}\n")
336
+ f.write("\n")
337
+ f.write("# Usage examples:\n")
338
+ f.write("# source .ensemble/panes.env\n")
339
+ f.write("# tmux send-keys -t \"$CONDUCTOR_PANE\" 'message' Enter\n")
340
+ f.write("# tmux send-keys -t \"$DISPATCH_PANE\" 'message' Enter\n")
341
+
342
+ click.echo(f" Saved pane IDs to {panes_env.relative_to(ensemble_dir.parent)}")
@@ -0,0 +1,36 @@
1
+ """Issue command for listing and viewing issues."""
2
+
3
+ import click
4
+
5
+
6
+ @click.command()
7
+ @click.argument("identifier", required=False)
8
+ @click.option(
9
+ "--state",
10
+ type=click.Choice(["open", "closed", "all"]),
11
+ default="open",
12
+ help="Filter issues by state.",
13
+ )
14
+ @click.option(
15
+ "--provider",
16
+ type=click.Choice(["github", "gitlab"]),
17
+ default="github",
18
+ help="Issue provider to use.",
19
+ )
20
+ def issue(identifier: str | None, state: str, provider: str) -> None:
21
+ """List issues or view a specific issue.
22
+
23
+ Without IDENTIFIER, lists all issues.
24
+ With IDENTIFIER, shows details of that issue (number or URL).
25
+
26
+ Examples:
27
+
28
+ ensemble issue # List open issues
29
+
30
+ ensemble issue 123 # View issue #123
31
+
32
+ ensemble issue --state all # List all issues
33
+ """
34
+ from ensemble.commands._issue_impl import run_issue
35
+
36
+ run_issue(identifier=identifier, state=state, provider=provider)