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.
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/.gitignore +3 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/PKG-INFO +8 -5
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/README.md +7 -4
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/pyproject.toml +1 -1
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/__init__.py +1 -1
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/cli.py +2 -0
- ensemble_claude-0.4.1/src/ensemble/commands/_issue_impl.py +150 -0
- ensemble_claude-0.4.1/src/ensemble/commands/_launch_impl.py +342 -0
- ensemble_claude-0.4.1/src/ensemble/commands/issue.py +36 -0
- ensemble_claude-0.4.1/src/ensemble/git_utils.py +157 -0
- ensemble_claude-0.4.1/src/ensemble/issue_provider.py +97 -0
- ensemble_claude-0.4.1/src/ensemble/providers/__init__.py +5 -0
- ensemble_claude-0.4.1/src/ensemble/providers/github.py +103 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/conductor.md +64 -10
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/dispatch.md +128 -14
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/worker.md +55 -0
- ensemble_claude-0.4.1/src/ensemble/templates/commands/go-issue.md +157 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/go.md +19 -4
- ensemble_claude-0.4.1/src/ensemble/templates/scripts/launch.sh +176 -0
- ensemble_claude-0.4.1/src/ensemble/templates/scripts/pane-setup.sh +184 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_cli.py +7 -2
- ensemble_claude-0.4.1/tests/test_git_utils.py +232 -0
- ensemble_claude-0.4.1/tests/test_issue_command.py +137 -0
- ensemble_claude-0.4.1/tests/test_issue_provider.py +265 -0
- ensemble_claude-0.3.1/src/ensemble/commands/_launch_impl.py +0 -217
- ensemble_claude-0.3.1/src/ensemble/templates/scripts/launch.sh +0 -137
- ensemble_claude-0.3.1/src/ensemble/templates/scripts/pane-setup.sh +0 -111
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/LICENSE +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/ack.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/__init__.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/_init_impl.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/init.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/commands/launch.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/config.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/dashboard.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/helpers.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/lock.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/logger.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/notes.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/queue.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/__init__.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/integrator.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/learner.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/reviewer.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/agents/security-reviewer.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/go-light.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/improve.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/review.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/commands/status.md +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/dashboard-update.sh +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/setup.sh +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/worktree-create.sh +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/scripts/worktree-merge.sh +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/default.yaml +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/heavy.yaml +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/simple.yaml +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/templates/workflows/worktree.yaml +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/utils.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/workflow.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/src/ensemble/worktree.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/__init__.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_ack.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_config.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_dashboard.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_lock.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_logger.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_notes.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_queue.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_setup.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_templates.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_workflow.py +0 -0
- {ensemble_claude-0.3.1 → ensemble_claude-0.4.1}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ensemble-claude
|
|
3
|
-
Version: 0.
|
|
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
|
|
90
|
+
# 2. Launch the tmux sessions (2 separate sessions)
|
|
91
91
|
ensemble launch
|
|
92
92
|
|
|
93
|
-
# 3.
|
|
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
|
|
107
|
-
| `ensemble launch --no-attach` | Start
|
|
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
|
|
62
|
+
# 2. Launch the tmux sessions (2 separate sessions)
|
|
63
63
|
ensemble launch
|
|
64
64
|
|
|
65
|
-
# 3.
|
|
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
|
|
79
|
-
| `ensemble launch --no-attach` | Start
|
|
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
|
|
@@ -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)
|