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.
- ensemble/__init__.py +5 -0
- ensemble/ack.py +86 -0
- ensemble/cli.py +31 -0
- ensemble/commands/__init__.py +1 -0
- ensemble/commands/_init_impl.py +208 -0
- ensemble/commands/_launch_impl.py +217 -0
- ensemble/commands/init.py +35 -0
- ensemble/commands/launch.py +32 -0
- ensemble/config.py +218 -0
- ensemble/dashboard.py +168 -0
- ensemble/helpers.py +79 -0
- ensemble/lock.py +77 -0
- ensemble/logger.py +80 -0
- ensemble/notes.py +221 -0
- ensemble/queue.py +166 -0
- ensemble/templates/__init__.py +75 -0
- ensemble/templates/agents/conductor.md +239 -0
- ensemble/templates/agents/dispatch.md +351 -0
- ensemble/templates/agents/integrator.md +138 -0
- ensemble/templates/agents/learner.md +133 -0
- ensemble/templates/agents/reviewer.md +84 -0
- ensemble/templates/agents/security-reviewer.md +136 -0
- ensemble/templates/agents/worker.md +184 -0
- ensemble/templates/commands/go-light.md +49 -0
- ensemble/templates/commands/go.md +101 -0
- ensemble/templates/commands/improve.md +116 -0
- ensemble/templates/commands/review.md +74 -0
- ensemble/templates/commands/status.md +56 -0
- ensemble/templates/scripts/dashboard-update.sh +78 -0
- ensemble/templates/scripts/launch.sh +137 -0
- ensemble/templates/scripts/pane-setup.sh +111 -0
- ensemble/templates/scripts/setup.sh +163 -0
- ensemble/templates/scripts/worktree-create.sh +89 -0
- ensemble/templates/scripts/worktree-merge.sh +194 -0
- ensemble/templates/workflows/default.yaml +78 -0
- ensemble/templates/workflows/heavy.yaml +149 -0
- ensemble/templates/workflows/simple.yaml +41 -0
- ensemble/templates/workflows/worktree.yaml +202 -0
- ensemble/utils.py +60 -0
- ensemble/workflow.py +127 -0
- ensemble/worktree.py +322 -0
- ensemble_claude-0.3.0.dist-info/METADATA +144 -0
- ensemble_claude-0.3.0.dist-info/RECORD +46 -0
- ensemble_claude-0.3.0.dist-info/WHEEL +4 -0
- ensemble_claude-0.3.0.dist-info/entry_points.txt +2 -0
- ensemble_claude-0.3.0.dist-info/licenses/LICENSE +21 -0
ensemble/utils.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
src/ensemble/utils.py - ユーティリティ関数
|
|
3
|
+
|
|
4
|
+
汎用的なユーティリティ関数を提供する。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_timestamp(dt: datetime | None = None) -> str:
|
|
12
|
+
"""
|
|
13
|
+
日時をISO8601形式の文字列に変換する
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
dt: 変換する日時。Noneの場合は現在時刻
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
ISO8601形式の文字列
|
|
20
|
+
"""
|
|
21
|
+
if dt is None:
|
|
22
|
+
dt = datetime.now()
|
|
23
|
+
return dt.isoformat()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def truncate_string(s: str, max_length: int = 100, suffix: str = "...") -> str:
|
|
27
|
+
"""
|
|
28
|
+
文字列を指定した長さに切り詰める
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
s: 切り詰める文字列
|
|
32
|
+
max_length: 最大長
|
|
33
|
+
suffix: 切り詰め時に追加する接尾辞
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
切り詰められた文字列
|
|
37
|
+
"""
|
|
38
|
+
if len(s) <= max_length:
|
|
39
|
+
return s
|
|
40
|
+
return s[: max_length - len(suffix)] + suffix
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
2つの辞書を深くマージする
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
base: ベースとなる辞書
|
|
49
|
+
override: 上書きする辞書
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
マージされた辞書
|
|
53
|
+
"""
|
|
54
|
+
result = base.copy()
|
|
55
|
+
for key, value in override.items():
|
|
56
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
57
|
+
result[key] = deep_merge(result[key], value)
|
|
58
|
+
else:
|
|
59
|
+
result[key] = value
|
|
60
|
+
return result
|
ensemble/workflow.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ワークフロー集約ロジックユーティリティ
|
|
3
|
+
|
|
4
|
+
注意: このモジュールは状態遷移を行わない。
|
|
5
|
+
状態遷移はClaude(Conductor)が担当する。
|
|
6
|
+
このモジュールは集約ロジック(all/any判定)のみを提供する。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 重大度の優先順位(ソート用)
|
|
17
|
+
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def aggregate_results(results: list[str], rule: str) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
並列レビュー結果を集約する
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
results: 各レビューアの結果 ["approved", "needs_fix", ...]
|
|
26
|
+
rule: 集約ルール "all(\"approved\")" or "any(\"needs_fix\")"
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ルールが満たされればTrue
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> aggregate_results(["approved", "approved"], 'all("approved")')
|
|
33
|
+
True
|
|
34
|
+
>>> aggregate_results(["approved", "needs_fix"], 'any("needs_fix")')
|
|
35
|
+
True
|
|
36
|
+
"""
|
|
37
|
+
# ルールをパース("all('xxx')" or 'all("xxx")' 形式に対応)
|
|
38
|
+
match = re.match(r'(all|any)\(["\']([^"\']+)["\']\)', rule)
|
|
39
|
+
if not match:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
operator, target = match.groups()
|
|
43
|
+
|
|
44
|
+
if operator == "all":
|
|
45
|
+
return all(r == target for r in results)
|
|
46
|
+
elif operator == "any":
|
|
47
|
+
return any(r == target for r in results)
|
|
48
|
+
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_review_results(reports_dir: str) -> dict[str, str]:
|
|
53
|
+
"""
|
|
54
|
+
queue/reports/ からレビュー結果を収集する
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
reports_dir: レポートディレクトリのパス
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
{"arch-review": "approved", "security-review": "needs_fix"}
|
|
61
|
+
"""
|
|
62
|
+
results: dict[str, str] = {}
|
|
63
|
+
reports_path = Path(reports_dir)
|
|
64
|
+
|
|
65
|
+
if not reports_path.exists():
|
|
66
|
+
return results
|
|
67
|
+
|
|
68
|
+
for report_file in reports_path.glob("*.yaml"):
|
|
69
|
+
try:
|
|
70
|
+
content = yaml.safe_load(report_file.read_text())
|
|
71
|
+
if content and isinstance(content, dict) and "result" in content:
|
|
72
|
+
# ファイル名からレビュー名を抽出(例: arch-review-task-123.yaml → arch-review)
|
|
73
|
+
filename = report_file.stem # 拡張子なし
|
|
74
|
+
# task-id部分を除去
|
|
75
|
+
parts = filename.rsplit("-", 2)
|
|
76
|
+
if len(parts) >= 3:
|
|
77
|
+
review_name = "-".join(parts[:-2]) # arch-review
|
|
78
|
+
else:
|
|
79
|
+
review_name = filename
|
|
80
|
+
results[review_name] = content["result"]
|
|
81
|
+
except yaml.YAMLError:
|
|
82
|
+
# 不正なYAMLはスキップ
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
return results
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def merge_findings(reports_dir: str) -> list[dict[str, Any]]:
|
|
89
|
+
"""
|
|
90
|
+
複数のレポートからfindingsをマージして重大度順にソートする
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
reports_dir: レポートディレクトリのパス
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
マージされたfindingsのリスト(重大度の高い順)
|
|
97
|
+
"""
|
|
98
|
+
all_findings: list[dict[str, Any]] = []
|
|
99
|
+
reports_path = Path(reports_dir)
|
|
100
|
+
|
|
101
|
+
if not reports_path.exists():
|
|
102
|
+
return all_findings
|
|
103
|
+
|
|
104
|
+
for report_file in reports_path.glob("*.yaml"):
|
|
105
|
+
try:
|
|
106
|
+
content = yaml.safe_load(report_file.read_text())
|
|
107
|
+
if content and isinstance(content, dict):
|
|
108
|
+
findings = content.get("findings", [])
|
|
109
|
+
# ファイル名からソース情報を取得
|
|
110
|
+
filename = report_file.stem
|
|
111
|
+
parts = filename.rsplit("-", 2)
|
|
112
|
+
if len(parts) >= 3:
|
|
113
|
+
source = "-".join(parts[:-2])
|
|
114
|
+
else:
|
|
115
|
+
source = filename
|
|
116
|
+
|
|
117
|
+
for finding in findings:
|
|
118
|
+
if isinstance(finding, dict):
|
|
119
|
+
finding["source"] = source
|
|
120
|
+
all_findings.append(finding)
|
|
121
|
+
except yaml.YAMLError:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# 重大度でソート(critical > high > medium > low)
|
|
125
|
+
all_findings.sort(key=lambda f: SEVERITY_ORDER.get(f.get("severity", "low"), 999))
|
|
126
|
+
|
|
127
|
+
return all_findings
|
ensemble/worktree.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
src/ensemble/worktree.py - git worktree操作ユーティリティ
|
|
3
|
+
|
|
4
|
+
worktreeの一覧取得、コンフリクト検出、レポート生成を行う。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class WorktreeInfo:
|
|
17
|
+
"""worktreeの情報"""
|
|
18
|
+
|
|
19
|
+
path: str
|
|
20
|
+
branch: str
|
|
21
|
+
head: str
|
|
22
|
+
is_bare: bool = False
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return f"WorktreeInfo({self.branch} @ {self.path})"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ConflictFile:
|
|
30
|
+
"""コンフリクトが発生したファイルの情報"""
|
|
31
|
+
|
|
32
|
+
file_path: str
|
|
33
|
+
conflict_type: str # "both_modified", "deleted_by_us", "deleted_by_them"
|
|
34
|
+
ours_content: str
|
|
35
|
+
theirs_content: str
|
|
36
|
+
auto_resolvable: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ConflictReport:
|
|
41
|
+
"""コンフリクトレポート"""
|
|
42
|
+
|
|
43
|
+
worktree_path: str
|
|
44
|
+
branch: str
|
|
45
|
+
main_branch: str
|
|
46
|
+
conflicts: list[ConflictFile] = field(default_factory=list)
|
|
47
|
+
has_conflicts: bool = False
|
|
48
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
49
|
+
|
|
50
|
+
def to_yaml(self) -> str:
|
|
51
|
+
"""YAML形式に変換"""
|
|
52
|
+
lines = [
|
|
53
|
+
"type: conflict",
|
|
54
|
+
f"timestamp: {self.timestamp}",
|
|
55
|
+
f"worktree: {self.worktree_path}",
|
|
56
|
+
f"branch: {self.branch}",
|
|
57
|
+
f"main_branch: {self.main_branch}",
|
|
58
|
+
f"has_conflicts: {str(self.has_conflicts).lower()}",
|
|
59
|
+
"conflict_files:",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for conflict in self.conflicts:
|
|
63
|
+
lines.extend(
|
|
64
|
+
[
|
|
65
|
+
f" - file: {conflict.file_path}",
|
|
66
|
+
f" type: {conflict.conflict_type}",
|
|
67
|
+
f" auto_resolvable: {str(conflict.auto_resolvable).lower()}",
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return "\n".join(lines)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_worktrees(repo_path: str) -> list[WorktreeInfo]:
|
|
75
|
+
"""
|
|
76
|
+
gitリポジトリのworktree一覧を取得する
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
repo_path: gitリポジトリのパス
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
WorktreeInfoのリスト(メインリポジトリは除外)
|
|
83
|
+
"""
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
["git", "worktree", "list"],
|
|
86
|
+
cwd=repo_path,
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if result.returncode != 0:
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
worktrees = []
|
|
95
|
+
lines = result.stdout.strip().split("\n")
|
|
96
|
+
|
|
97
|
+
for i, line in enumerate(lines):
|
|
98
|
+
if not line.strip():
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# パース: /path/to/worktree abc1234 [branch-name]
|
|
102
|
+
match = re.match(r"^(\S+)\s+([a-f0-9]+)\s+\[(.+)\]$", line.strip())
|
|
103
|
+
if match:
|
|
104
|
+
path, head, branch = match.groups()
|
|
105
|
+
|
|
106
|
+
# 最初の行(メインリポジトリ)は除外
|
|
107
|
+
if i == 0:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
worktrees.append(
|
|
111
|
+
WorktreeInfo(
|
|
112
|
+
path=path,
|
|
113
|
+
branch=branch,
|
|
114
|
+
head=head,
|
|
115
|
+
is_bare=False,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return worktrees
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_worktree_branch(worktree_path: str) -> str:
|
|
123
|
+
"""
|
|
124
|
+
worktreeのブランチ名を取得
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
worktree_path: worktreeのパス
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
ブランチ名
|
|
131
|
+
"""
|
|
132
|
+
result = subprocess.run(
|
|
133
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
134
|
+
cwd=worktree_path,
|
|
135
|
+
capture_output=True,
|
|
136
|
+
text=True,
|
|
137
|
+
)
|
|
138
|
+
return result.stdout.strip()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def detect_conflicts(
|
|
142
|
+
worktree_path: str,
|
|
143
|
+
branch: str,
|
|
144
|
+
main_branch: str = "main",
|
|
145
|
+
) -> ConflictReport:
|
|
146
|
+
"""
|
|
147
|
+
worktreeをメインブランチにマージする際のコンフリクトを検出
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
worktree_path: worktreeのパス
|
|
151
|
+
branch: マージするブランチ名
|
|
152
|
+
main_branch: マージ先のブランチ名
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
ConflictReport
|
|
156
|
+
"""
|
|
157
|
+
# dry-runでマージを試行
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
["git", "merge", "--no-commit", "--no-ff", branch],
|
|
160
|
+
cwd=worktree_path,
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if result.returncode == 0:
|
|
166
|
+
# コンフリクトなし - マージをアボート
|
|
167
|
+
subprocess.run(
|
|
168
|
+
["git", "merge", "--abort"],
|
|
169
|
+
cwd=worktree_path,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
)
|
|
172
|
+
return ConflictReport(
|
|
173
|
+
worktree_path=worktree_path,
|
|
174
|
+
branch=branch,
|
|
175
|
+
main_branch=main_branch,
|
|
176
|
+
has_conflicts=False,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# コンフリクトあり - ファイル一覧を取得
|
|
180
|
+
diff_result = subprocess.run(
|
|
181
|
+
["git", "diff", "--name-only", "--diff-filter=U"],
|
|
182
|
+
cwd=worktree_path,
|
|
183
|
+
capture_output=True,
|
|
184
|
+
text=True,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
conflict_files = []
|
|
188
|
+
for file_path in diff_result.stdout.strip().split("\n"):
|
|
189
|
+
if not file_path:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
# ファイル内容を読み取り
|
|
193
|
+
full_path = Path(worktree_path) / file_path
|
|
194
|
+
if full_path.exists():
|
|
195
|
+
content = full_path.read_text()
|
|
196
|
+
ours, theirs = parse_conflict_markers(content)
|
|
197
|
+
auto_resolvable = is_auto_resolvable(file_path, ours, theirs)
|
|
198
|
+
else:
|
|
199
|
+
ours, theirs = "", ""
|
|
200
|
+
auto_resolvable = False
|
|
201
|
+
|
|
202
|
+
conflict_files.append(
|
|
203
|
+
ConflictFile(
|
|
204
|
+
file_path=file_path,
|
|
205
|
+
conflict_type="both_modified",
|
|
206
|
+
ours_content=ours,
|
|
207
|
+
theirs_content=theirs,
|
|
208
|
+
auto_resolvable=auto_resolvable,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# マージをアボート
|
|
213
|
+
subprocess.run(
|
|
214
|
+
["git", "merge", "--abort"],
|
|
215
|
+
cwd=worktree_path,
|
|
216
|
+
capture_output=True,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return ConflictReport(
|
|
220
|
+
worktree_path=worktree_path,
|
|
221
|
+
branch=branch,
|
|
222
|
+
main_branch=main_branch,
|
|
223
|
+
conflicts=conflict_files,
|
|
224
|
+
has_conflicts=True,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def parse_conflict_markers(content: str) -> tuple[str, str]:
|
|
229
|
+
"""
|
|
230
|
+
コンフリクトマーカーをパースして、ours/theirsの内容を抽出
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
content: ファイル内容
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
(ours_content, theirs_content) のタプル
|
|
237
|
+
"""
|
|
238
|
+
# <<<<<<< HEAD
|
|
239
|
+
# ours content
|
|
240
|
+
# =======
|
|
241
|
+
# theirs content
|
|
242
|
+
# >>>>>>> branch
|
|
243
|
+
pattern = r"<<<<<<< .*?\n(.*?)\n=======\n(.*?)\n>>>>>>> .*?"
|
|
244
|
+
|
|
245
|
+
match = re.search(pattern, content, re.DOTALL)
|
|
246
|
+
if match:
|
|
247
|
+
return match.group(1).strip(), match.group(2).strip()
|
|
248
|
+
|
|
249
|
+
return "", ""
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def is_auto_resolvable(
|
|
253
|
+
file_path: str, ours_content: str, theirs_content: str
|
|
254
|
+
) -> bool:
|
|
255
|
+
"""
|
|
256
|
+
コンフリクトが自動解決可能かどうかを判定
|
|
257
|
+
|
|
258
|
+
自動解決可能なケース:
|
|
259
|
+
- インポート文の追加
|
|
260
|
+
- 独立した関数/クラスの追加
|
|
261
|
+
|
|
262
|
+
自動解決不可のケース:
|
|
263
|
+
- 同じ関数/クラスの異なる修正
|
|
264
|
+
- 設定値の競合
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
file_path: ファイルパス
|
|
268
|
+
ours_content: oursの内容
|
|
269
|
+
theirs_content: theirsの内容
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
自動解決可能ならTrue
|
|
273
|
+
"""
|
|
274
|
+
if not ours_content or not theirs_content:
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
# インポート文のみの場合は自動解決可能
|
|
278
|
+
import_pattern = r"^(import |from .* import )"
|
|
279
|
+
ours_is_import = bool(re.match(import_pattern, ours_content.strip()))
|
|
280
|
+
theirs_is_import = bool(re.match(import_pattern, theirs_content.strip()))
|
|
281
|
+
|
|
282
|
+
if ours_is_import and theirs_is_import:
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
# 独立した関数/クラス定義の追加
|
|
286
|
+
def_pattern = r"^(def |class |async def )"
|
|
287
|
+
ours_is_def = bool(re.match(def_pattern, ours_content.strip()))
|
|
288
|
+
theirs_is_def = bool(re.match(def_pattern, theirs_content.strip()))
|
|
289
|
+
|
|
290
|
+
if ours_is_def and theirs_is_def:
|
|
291
|
+
# 同じ名前の関数/クラスでなければ自動解決可能
|
|
292
|
+
ours_name = _extract_def_name(ours_content)
|
|
293
|
+
theirs_name = _extract_def_name(theirs_content)
|
|
294
|
+
if ours_name and theirs_name and ours_name != theirs_name:
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
# 設定ファイルの同じキーは自動解決不可
|
|
298
|
+
if "config" in file_path.lower() or "settings" in file_path.lower():
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _extract_def_name(content: str) -> Optional[str]:
|
|
305
|
+
"""関数/クラス名を抽出"""
|
|
306
|
+
match = re.match(r"^(?:async )?(?:def|class)\s+(\w+)", content.strip())
|
|
307
|
+
if match:
|
|
308
|
+
return match.group(1)
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def generate_conflict_report(report: ConflictReport, output_path: str) -> None:
|
|
313
|
+
"""
|
|
314
|
+
コンフリクトレポートをファイルに出力
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
report: ConflictReport
|
|
318
|
+
output_path: 出力先パス
|
|
319
|
+
"""
|
|
320
|
+
output = Path(output_path)
|
|
321
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
322
|
+
output.write_text(report.to_yaml())
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ensemble-claude
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: AI Orchestration Tool for Claude Code - Multi-agent orchestration for complex development tasks
|
|
5
|
+
Project-URL: Homepage, https://github.com/ChikaKakazu/ensemble
|
|
6
|
+
Project-URL: Documentation, https://github.com/ChikaKakazu/ensemble#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/ChikaKakazu/ensemble
|
|
8
|
+
Project-URL: Issues, https://github.com/ChikaKakazu/ensemble/issues
|
|
9
|
+
Author-email: Chika Kakazu <chika@example.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,automation,claude,multi-agent,orchestration
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: click>=8.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Ensemble
|
|
30
|
+
|
|
31
|
+
> **"One task. Many minds. One result."**
|
|
32
|
+
|
|
33
|
+
AI Orchestration Tool for Claude Code.
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
Ensemble is an AI orchestration system that combines the best practices from:
|
|
38
|
+
- **shogun** - Autonomous AI collaboration with tmux parallel execution
|
|
39
|
+
- **takt** - Workflow enforcement with quality gates
|
|
40
|
+
- **Boris's practices** - Effective use of skills, subagents, CLAUDE.md, and hooks
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Autonomous AI Coordination**: One instruction triggers multiple AI agents working together
|
|
45
|
+
- **Flexible Execution Patterns**:
|
|
46
|
+
- Pattern A: Simple tasks via subagent
|
|
47
|
+
- Pattern B: Medium tasks via tmux parallel panes
|
|
48
|
+
- Pattern C: Large tasks via git worktree separation
|
|
49
|
+
- **Parallel Review**: Architecture + Security reviews run in parallel
|
|
50
|
+
- **Self-Improvement**: Automatic learning and CLAUDE.md updates
|
|
51
|
+
- **Compaction Recovery**: Built-in protocol to prevent role amnesia
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
### Using uv (recommended)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Install globally
|
|
59
|
+
uv tool install ensemble-claude
|
|
60
|
+
|
|
61
|
+
# Or add to your project
|
|
62
|
+
uv add ensemble-claude
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Using pip
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install ensemble-claude
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### From source
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/ChikaKakazu/ensemble.git
|
|
75
|
+
cd ensemble
|
|
76
|
+
|
|
77
|
+
# Using uv
|
|
78
|
+
uv pip install -e .
|
|
79
|
+
|
|
80
|
+
# Or using pip
|
|
81
|
+
pip install -e .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 1. Initialize Ensemble in your project
|
|
88
|
+
ensemble init
|
|
89
|
+
|
|
90
|
+
# 2. Launch the tmux session with Conductor + Dispatch
|
|
91
|
+
ensemble launch
|
|
92
|
+
|
|
93
|
+
# 3. Run a task (in the Conductor pane)
|
|
94
|
+
/go implement user authentication
|
|
95
|
+
|
|
96
|
+
# Light workflow (minimal cost)
|
|
97
|
+
/go-light fix typo in README
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### CLI Commands
|
|
101
|
+
|
|
102
|
+
| Command | Description |
|
|
103
|
+
|---------|-------------|
|
|
104
|
+
| `ensemble init` | Initialize Ensemble in current project |
|
|
105
|
+
| `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 |
|
|
108
|
+
| `ensemble --version` | Show version |
|
|
109
|
+
|
|
110
|
+
## Requirements
|
|
111
|
+
|
|
112
|
+
- Python 3.10+
|
|
113
|
+
- Claude Code CLI (`claude` command available)
|
|
114
|
+
- tmux
|
|
115
|
+
- git 2.20+ (for worktree support)
|
|
116
|
+
- Claude Max plan recommended (for parallel execution)
|
|
117
|
+
|
|
118
|
+
## Agent Architecture
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
┌─────────────┐
|
|
122
|
+
│ Conductor │ ← Orchestrator (planning, judgment, delegation)
|
|
123
|
+
└──────┬──────┘
|
|
124
|
+
│
|
|
125
|
+
┌────┴────┐
|
|
126
|
+
▼ ▼
|
|
127
|
+
┌────────┐ ┌──────────┐
|
|
128
|
+
│Dispatch│ │ Learner │
|
|
129
|
+
└───┬────┘ └──────────┘
|
|
130
|
+
│ ↑ Learning records
|
|
131
|
+
▼
|
|
132
|
+
┌─────────────────────────────┐
|
|
133
|
+
│ Reviewer / Security-Reviewer│ ← Parallel reviews
|
|
134
|
+
└─────────────────────────────┘
|
|
135
|
+
│
|
|
136
|
+
▼ (worktree mode)
|
|
137
|
+
┌──────────┐
|
|
138
|
+
│Integrator│ ← Merge & integrate
|
|
139
|
+
└──────────┘
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
ensemble/__init__.py,sha256=uu-cJetK9gtjeysi3P4JIKUG2V9qyY_oNZqTl0Z7NTU,130
|
|
2
|
+
ensemble/ack.py,sha256=IfgbydQHhwwGivlr4IkBpOmruDvV8U7i4C2T-c5bJtY,2201
|
|
3
|
+
ensemble/cli.py,sha256=y_4jV9WxSpbWYe8I04hIgNGvTsinsm4CC0oqnXem_R8,680
|
|
4
|
+
ensemble/config.py,sha256=8Dck2ZShM1duwRe_GbSgKXA0zhUfzobYEoZCrQPRabI,6163
|
|
5
|
+
ensemble/dashboard.py,sha256=kvkxl50F7JwFVj9OGNqeeNnFAnL8q-5L-TFsJMLLA1w,4530
|
|
6
|
+
ensemble/helpers.py,sha256=lqyxV5Dl-wXxaGHL_1Xl1gCw4CKOJP66S12m6k9Gurk,1910
|
|
7
|
+
ensemble/lock.py,sha256=tXj-s-xkGEFk9vVStyffUMb8X1fTFiqOWnFdNDhdnxU,2265
|
|
8
|
+
ensemble/logger.py,sha256=4WgdF2SR8tV5ibJmVEc6JT6KW-qhENMaLeS-pqkIl7Y,2466
|
|
9
|
+
ensemble/notes.py,sha256=gI9HcB7X3YTok_PsPxjH7h_r_-GJUC8tRyZoiclu4VM,6155
|
|
10
|
+
ensemble/queue.py,sha256=0ABMf1c2KhlsDWCfxziGBZcekJ5AjCz1qc82QkrkR20,4802
|
|
11
|
+
ensemble/utils.py,sha256=w62dc_Fsfu8QlezYOi7kwFfYtmE8PtRgIjNUB6mVi9s,1482
|
|
12
|
+
ensemble/workflow.py,sha256=nna9Kqs6rEzH6jNbe8LoY4yr_1ZHj5eXwbOUFDi3NL8,4056
|
|
13
|
+
ensemble/worktree.py,sha256=Lru06_XYMXnz2I6lu_p3LribzmtZbKucukEFGp6Vu2w,8742
|
|
14
|
+
ensemble/commands/__init__.py,sha256=DKpBmPiD8IsT2CVzGneGl95yoNy8npDKF7P2JO__s3k,29
|
|
15
|
+
ensemble/commands/_init_impl.py,sha256=Zy-eQPHoy7IEXPQ2n3aFM4R9ohhAVp3MthdfNOqR7aE,6729
|
|
16
|
+
ensemble/commands/_launch_impl.py,sha256=6GidjPHLF-qcvwbAi1sUf2PZQb90hV9fHVD0_fReerU,6742
|
|
17
|
+
ensemble/commands/init.py,sha256=TqFUIeRlnxSnuKrhgVQz5Dq2zAYG0tSMXj_pYcDDI-E,975
|
|
18
|
+
ensemble/commands/launch.py,sha256=AhLKQUZBCVT9bILmQfFLcgQ8snFKJEIKXQeOeTp4sC0,937
|
|
19
|
+
ensemble/templates/__init__.py,sha256=3qMoDLzoWw1aAH-pVd0Zy_g-AuVjpNwDp5GiGguqH2s,2285
|
|
20
|
+
ensemble/templates/agents/conductor.md,sha256=-_If43BYIQv15FGtTAaMNNsECdFC_Oyfl2gfKg_0J2Y,8500
|
|
21
|
+
ensemble/templates/agents/dispatch.md,sha256=EAAejqKQRcmo6qVb1tTwj2wUfOIpqQl_TQ2H8SsQoeA,11033
|
|
22
|
+
ensemble/templates/agents/integrator.md,sha256=HV11ZtpWsd2r9f_0lF4KaW9tIFlMT10EqX8ph-4d-kg,4064
|
|
23
|
+
ensemble/templates/agents/learner.md,sha256=WQc7byrFLbUMDy24UtqG2K-vYe2npZX_meyfuWrPW-I,3344
|
|
24
|
+
ensemble/templates/agents/reviewer.md,sha256=_ArrOg_GusMbjaRszQYNGPfedyXnvr0jGUnuv6xPAZw,2256
|
|
25
|
+
ensemble/templates/agents/security-reviewer.md,sha256=GjObV4ljxxkYs9lmi3uV2_iuR5P9a_PldXxNl9CMRmk,3884
|
|
26
|
+
ensemble/templates/agents/worker.md,sha256=zH5SCMq2KirpYRa6MlfOWNWdMoXUdboLG1QiasaMoUI,5980
|
|
27
|
+
ensemble/templates/commands/go-light.md,sha256=z4LFtyku3LPeomfCAgVbmXkn5BMXkBE6xvqmjwB-Bso,1239
|
|
28
|
+
ensemble/templates/commands/go.md,sha256=9qV_1ccuDpCWQVrMhjb1eljvwUV4Pzyv_4vD9lgH1Hw,3749
|
|
29
|
+
ensemble/templates/commands/improve.md,sha256=qSLLnYwfVedJwH8pCaJfVM7DtNBcgXL-s9fCJLTkN2s,2876
|
|
30
|
+
ensemble/templates/commands/review.md,sha256=H0eSxIHdWE3LOfWRRfcamYyEvg1RJYy98G8hW5352lY,2054
|
|
31
|
+
ensemble/templates/commands/status.md,sha256=rbYfVAwRfYy9EJOSADpAgVD46guPL2IS6F4768PLGAs,1296
|
|
32
|
+
ensemble/templates/scripts/dashboard-update.sh,sha256=NV2S2z2zHw42NaMsKEaN9vANAui8W0brEtSmKwOMs8A,2453
|
|
33
|
+
ensemble/templates/scripts/launch.sh,sha256=G91x_QUzUBoQoL-Ri47blrqNuWEhiiP4HZTCYInAtCY,4797
|
|
34
|
+
ensemble/templates/scripts/pane-setup.sh,sha256=bCCTL_FQjNZeTl_64g1oKG-1T6BrOuY9Gc_yYRkycaI,3583
|
|
35
|
+
ensemble/templates/scripts/setup.sh,sha256=2MBebHJ3y9ogl28DdH_9d6gxBFBvziOvk-orEAwXIPw,4190
|
|
36
|
+
ensemble/templates/scripts/worktree-create.sh,sha256=xQgU-LymXu_K0PVBWOmyqePfvYkJOcZN5iK_hJ9x40Y,2574
|
|
37
|
+
ensemble/templates/scripts/worktree-merge.sh,sha256=_vl7kJvGKxWbiC5BTxnALIVV_FyHP010GaHkglncM3M,5115
|
|
38
|
+
ensemble/templates/workflows/default.yaml,sha256=TXhXgHGwCoc28cepHFUzO-zjZU3-tLVGotB4KDIGv2w,2176
|
|
39
|
+
ensemble/templates/workflows/heavy.yaml,sha256=JGXXfo0DqyrFBMQyI7NJ2WG60ENrcfSkhIlPhtW8akc,4669
|
|
40
|
+
ensemble/templates/workflows/simple.yaml,sha256=Ud3QzumP3Ykxb4rDmqegbkyIIMVIGHehAm1xi27pah4,987
|
|
41
|
+
ensemble/templates/workflows/worktree.yaml,sha256=o4pvOuq4wXNMTAWoX5ea39dR4NbwkaiXsqRwoWVEF2Q,5425
|
|
42
|
+
ensemble_claude-0.3.0.dist-info/METADATA,sha256=lr2GpwC92yKHFXgMSXXsQnVLJksKm-tZmD87VzNQuO0,4263
|
|
43
|
+
ensemble_claude-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
44
|
+
ensemble_claude-0.3.0.dist-info/entry_points.txt,sha256=2hiB0bL46igs1IiVtcNlXGzyX36CYciEI74_jiGbAAs,47
|
|
45
|
+
ensemble_claude-0.3.0.dist-info/licenses/LICENSE,sha256=XfPiMYh1DrSMFiJf3NmaKTfn1XrrZKcEP0OWp93_SpA,1069
|
|
46
|
+
ensemble_claude-0.3.0.dist-info/RECORD,,
|