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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any