ensemble-claude 0.3.0__py3-none-any.whl → 0.4.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.
@@ -0,0 +1,97 @@
1
+ """Abstract base class for issue providers (GitHub, GitLab, etc.)."""
2
+
3
+ import re
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class Issue:
10
+ """Represents an issue from a code hosting platform."""
11
+
12
+ number: int
13
+ title: str
14
+ body: str
15
+ url: str
16
+ state: str
17
+ labels: list[str]
18
+
19
+ def branch_slug(self, max_length: int = 50) -> str:
20
+ """Generate a git branch name slug from the issue.
21
+
22
+ Format: issue/<number>-<slugified-title>
23
+
24
+ Args:
25
+ max_length: Maximum length of the slug portion (excluding prefix).
26
+
27
+ Returns:
28
+ A valid git branch name.
29
+ """
30
+ # Convert title to lowercase
31
+ slug = self.title.lower()
32
+
33
+ # Replace special characters with spaces
34
+ slug = re.sub(r"[^a-z0-9\s-]", "", slug)
35
+
36
+ # Replace whitespace with hyphens
37
+ slug = re.sub(r"\s+", "-", slug)
38
+
39
+ # Remove consecutive hyphens
40
+ slug = re.sub(r"-+", "-", slug)
41
+
42
+ # Trim hyphens from ends
43
+ slug = slug.strip("-")
44
+
45
+ # Truncate to max length
46
+ if len(slug) > max_length:
47
+ # Try to cut at a word boundary
48
+ slug = slug[:max_length].rsplit("-", 1)[0]
49
+
50
+ return f"issue/{self.number}-{slug}"
51
+
52
+
53
+ class IssueProvider(ABC):
54
+ """Abstract base class for issue providers.
55
+
56
+ Implementations should handle specific platforms like GitHub, GitLab, etc.
57
+ """
58
+
59
+ @abstractmethod
60
+ def list_issues(self, state: str = "open") -> list[Issue]:
61
+ """List issues in the repository.
62
+
63
+ Args:
64
+ state: Filter by issue state ('open', 'closed', 'all').
65
+
66
+ Returns:
67
+ List of Issue objects.
68
+
69
+ Raises:
70
+ RuntimeError: If the operation fails.
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ def get_issue(self, identifier: str) -> Issue:
76
+ """Get a specific issue by number or URL.
77
+
78
+ Args:
79
+ identifier: Issue number (as string) or full URL.
80
+
81
+ Returns:
82
+ Issue object.
83
+
84
+ Raises:
85
+ ValueError: If the issue is not found.
86
+ RuntimeError: If the operation fails.
87
+ """
88
+ pass
89
+
90
+ @abstractmethod
91
+ def is_available(self) -> bool:
92
+ """Check if the provider's CLI tool is available.
93
+
94
+ Returns:
95
+ True if the CLI tool is installed and accessible.
96
+ """
97
+ pass
@@ -0,0 +1,5 @@
1
+ """Issue providers for different code hosting platforms."""
2
+
3
+ from ensemble.providers.github import GitHubProvider
4
+
5
+ __all__ = ["GitHubProvider"]
@@ -0,0 +1,103 @@
1
+ """GitHub issue provider using gh CLI."""
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from typing import Any
7
+
8
+ from ensemble.issue_provider import Issue, IssueProvider
9
+
10
+
11
+ class GitHubProvider(IssueProvider):
12
+ """Issue provider for GitHub using the gh CLI."""
13
+
14
+ def is_available(self) -> bool:
15
+ """Check if gh CLI is installed.
16
+
17
+ Returns:
18
+ True if gh is available in PATH.
19
+ """
20
+ return shutil.which("gh") is not None
21
+
22
+ def list_issues(self, state: str = "open") -> list[Issue]:
23
+ """List issues using gh CLI.
24
+
25
+ Args:
26
+ state: Filter by issue state ('open', 'closed', 'all').
27
+
28
+ Returns:
29
+ List of Issue objects.
30
+
31
+ Raises:
32
+ RuntimeError: If gh command fails.
33
+ """
34
+ cmd = [
35
+ "gh", "issue", "list",
36
+ "--state", state,
37
+ "--json", "number,title,body,url,state,labels",
38
+ ]
39
+
40
+ result = subprocess.run(
41
+ cmd,
42
+ capture_output=True,
43
+ text=True,
44
+ )
45
+
46
+ if result.returncode != 0:
47
+ raise RuntimeError(f"Failed to list issues: {result.stderr}")
48
+
49
+ data = json.loads(result.stdout)
50
+ return [self._parse_issue(item) for item in data]
51
+
52
+ def get_issue(self, identifier: str) -> Issue:
53
+ """Get a specific issue by number or URL.
54
+
55
+ Args:
56
+ identifier: Issue number (as string) or full URL.
57
+
58
+ Returns:
59
+ Issue object.
60
+
61
+ Raises:
62
+ ValueError: If the issue is not found.
63
+ RuntimeError: If gh command fails.
64
+ """
65
+ cmd = [
66
+ "gh", "issue", "view", identifier,
67
+ "--json", "number,title,body,url,state,labels",
68
+ ]
69
+
70
+ result = subprocess.run(
71
+ cmd,
72
+ capture_output=True,
73
+ text=True,
74
+ )
75
+
76
+ if result.returncode != 0:
77
+ if "not found" in result.stderr.lower() or "could not find" in result.stderr.lower():
78
+ raise ValueError(f"Issue not found: {identifier}")
79
+ raise RuntimeError(f"Failed to get issue: {result.stderr}")
80
+
81
+ data = json.loads(result.stdout)
82
+ return self._parse_issue(data)
83
+
84
+ def _parse_issue(self, data: dict[str, Any]) -> Issue:
85
+ """Parse issue data from gh JSON output.
86
+
87
+ Args:
88
+ data: Dictionary from gh JSON output.
89
+
90
+ Returns:
91
+ Issue object.
92
+ """
93
+ # Labels come as list of dicts with 'name' key
94
+ labels = [label["name"] for label in data.get("labels", [])]
95
+
96
+ return Issue(
97
+ number=data["number"],
98
+ title=data["title"],
99
+ body=data.get("body", ""),
100
+ url=data["url"],
101
+ state=data["state"].lower(),
102
+ labels=labels,
103
+ )
@@ -45,14 +45,15 @@ model: opus
45
45
 
46
46
  ## パターン別実行方法
47
47
 
48
- ### パターンA: takt方式(subagent直接)
48
+ ### パターンA: 単一Worker実行
49
49
 
50
- Taskツールでsubagentを起動して直接実行する。Dispatchは不要。
50
+ 軽量タスクでもDispatch経由で実行する。Conductorは計画・判断・委譲のみ。
51
51
 
52
52
  ```
53
- 1. Task(subagent_type="general-purpose")でタスク実行
54
- 2. 結果を確認
55
- 3. 必要に応じてレビュー
53
+ 1. queue/conductor/dispatch-instruction.yaml に指示を書く
54
+ 2. worker_count: 1 で単一Workerを指定
55
+ 3. Dispatchに通知し、Workerが実行
56
+ 4. 完了報告を待機
56
57
  ```
57
58
 
58
59
  ### パターンB: shogun方式(tmux並列)
@@ -201,13 +202,25 @@ Claude Max 5並列制限を考慮:
201
202
  |---------|--------|-----------|
202
203
  | `/go` または タスク依頼 | ユーザー | 計画立案・パターン選択・実行 |
203
204
 
204
- ### 完了確認方法(Dispatchからのsend-keysは来ない)
205
+ ### 完了確認方法(ポーリング)
205
206
 
206
- Dispatchはsend-keysでConductorに報告しない。以下の方法で完了を確認:
207
- 1. `status/dashboard.md` を定期的に確認
208
- 2. `queue/reports/` にファイルが揃ったら完了
207
+ Dispatchへの委譲後、以下のポーリング処理を実行:
209
208
 
210
- これにより、send-keysの信頼性問題を回避する。
209
+ ```bash
210
+ # 完了待機ループ(30秒間隔、最大30分)
211
+ for i in $(seq 1 60); do
212
+ if [ -f "queue/reports/completion-summary.yaml" ]; then
213
+ echo "タスク完了を検知"
214
+ break
215
+ fi
216
+ sleep 30
217
+ done
218
+ ```
219
+
220
+ 完了検知後:
221
+ 1. `queue/reports/completion-summary.yaml` を読み込む
222
+ 2. 結果をユーザーに報告
223
+ 3. completion-summary.yaml を削除(次回の検知のため)
211
224
 
212
225
  ## 自律判断チェックリスト
213
226
 
@@ -227,6 +240,47 @@ Dispatchはsend-keysでConductorに報告しない。以下の方法で完了を
227
240
  - [ ] 未完了タスクの棚卸し
228
241
  - [ ] queue/ 内の古いファイル削除
229
242
 
243
+ ## セッション構成
244
+
245
+ Ensembleは2つの独立したtmuxセッションで動作する:
246
+
247
+ ```
248
+ セッション1: ensemble-conductor
249
+ +------------------+------------------+
250
+ | Conductor | dashboard |
251
+ +------------------+------------------+
252
+
253
+ セッション2: ensemble-workers
254
+ +------------------+----------+
255
+ | | worker-1 |
256
+ | dispatch +----------+
257
+ | | worker-2 |
258
+ +------------------+----------+
259
+ ```
260
+
261
+ ### 2つのターミナルで同時表示
262
+
263
+ 別々のターミナルウィンドウで各セッションをアタッチすることで、
264
+ Conductor(+dashboard)とWorkers(dispatch+workers)両方を同時に監視できる:
265
+
266
+ ```bash
267
+ # ターミナル1
268
+ tmux attach -t ensemble-conductor
269
+
270
+ # ターミナル2
271
+ tmux attach -t ensemble-workers
272
+ ```
273
+
274
+ ### セッション間通信
275
+
276
+ セッションが分かれていてもsend-keysで通信可能:
277
+
278
+ ```bash
279
+ source .ensemble/panes.env
280
+ tmux send-keys -t "$CONDUCTOR_PANE" 'message' Enter
281
+ tmux send-keys -t "$DISPATCH_PANE" 'message' Enter
282
+ ```
283
+
230
284
  ## 禁止事項
231
285
 
232
286
  - 自分でコードを書く
@@ -78,13 +78,35 @@ ls queue/reports/*.yaml
78
78
  ```
79
79
  通信ロストした他ワーカーの報告も拾える。
80
80
 
81
- ## Conductor への報告方法(send-keys禁止)
81
+ ## Conductor への報告方法(ファイルベース + 通知)
82
82
 
83
- **Conductorにsend-keysを送ってはならない**。結果は以下の方法で報告:
84
- 1. `status/dashboard.md` を更新(全タスク完了時)
85
- 2. `queue/reports/` に報告ファイルを配置
83
+ ### 1. ファイル報告(プライマリ)
84
+ **Conductorにsend-keysを送らない**。結果はファイルで報告:
85
+ 1. `status/dashboard.md` を更新(完了ステータスに)
86
+ 2. `queue/reports/completion-summary.yaml` に集約結果を記載
86
87
 
87
- Conductorは自分でダッシュボードまたはファイル監視で状況を把握する。
88
+ ### 2. 通知(セカンダリ)
89
+ completion-summary.yaml作成後、Conductorに完了を通知:
90
+
91
+ #### 通知手順
92
+ ```bash
93
+ # panes.envを読み込む
94
+ source .ensemble/panes.env
95
+
96
+ # Conductorに通知(2回分割)
97
+ tmux send-keys -t "$CONDUCTOR_PANE" '全タスク完了。queue/reports/completion-summary.yamlを確認してください'
98
+ sleep 1
99
+ tmux send-keys -t "$CONDUCTOR_PANE" Enter
100
+ ```
101
+
102
+ #### 通知失敗時
103
+ - send-keysが失敗してもエラーにしない
104
+ - Conductorがポーリングでフォールバック
105
+ - ファイル報告が最優先(通知は補助)
106
+
107
+ ### 3. ポーリング(フォールバック)
108
+ Conductorは `queue/reports/completion-summary.yaml` の存在を検知して完了を把握する。
109
+ 通知は効率化のための補助機能。
88
110
 
89
111
  ---
90
112
 
@@ -155,22 +177,46 @@ workflow: default
155
177
  pattern: B
156
178
  ```
157
179
 
158
- ## ペイン構成
180
+ ## ウィンドウ・ペイン構成
181
+
182
+ Ensembleは2ウィンドウ構成で動作する:
183
+
184
+ ```
185
+ ウィンドウ1: conductor(Conductorがいる場所)
186
+ +----------------------------------+
187
+ | Conductor |
188
+ +----------------------------------+
189
+
190
+ ウィンドウ2: workers(あなたがいる場所)
191
+ +------------------+----------+
192
+ | dispatch | worker-1 |
193
+ | (あなた) +----------+
194
+ +------------------+ worker-2 |
195
+ | dashboard +----------+
196
+ | | ... |
197
+ +------------------+----------+
198
+ ```
159
199
 
160
200
  ペイン番号ではなくペインIDで管理する。`.ensemble/panes.env` を参照。
161
201
 
162
202
  ```
203
+ ウィンドウ名:
204
+ $CONDUCTOR_WINDOW: conductor
205
+ $WORKERS_WINDOW: workers
206
+
163
207
  初期状態:
164
- $CONDUCTOR_PANE: Conductor
208
+ $CONDUCTOR_PANE: Conductor(別ウィンドウ)
165
209
  $DISPATCH_PANE: Dispatch(自分)
166
210
  $DASHBOARD_PANE: Dashboard
211
+ $WORKER_AREA_PANE: ワーカー用プレースホルダー
167
212
 
168
213
  ワーカー追加後(例: 2ワーカー):
169
- $CONDUCTOR_PANE: Conductor
214
+ $CONDUCTOR_PANE: Conductor(別ウィンドウ)
170
215
  $DISPATCH_PANE: Dispatch(自分)
216
+ $DASHBOARD_PANE: Dashboard
171
217
  $WORKER_1_PANE: Worker-1 (WORKER_ID=1)
172
218
  $WORKER_2_PANE: Worker-2 (WORKER_ID=2)
173
- $DASHBOARD_PANE: Dashboard
219
+ $WORKER_COUNT: 2
174
220
  ```
175
221
 
176
222
  ## タスク配信の具体手順
@@ -218,13 +264,39 @@ done
218
264
 
219
265
  ## 完了報告の収集
220
266
 
267
+ ### プライマリ: 通知ベース
268
+ Workerから「タスク${TASK_ID}完了」の通知を受けたら、
269
+ queue/reports/${TASK_ID}-completed.yaml を確認する。
270
+
271
+ ### フォールバック: ポーリング
272
+ タイムアウト(例: 3分)後、Workerから通知がない場合:
273
+ 1. queue/reports/ を全スキャン
274
+ 2. 新しい完了報告ファイルを検出
275
+ 3. 未検出のタスクは「進行中」と判断し、追加で2分待機
276
+ 4. 再度タイムアウト → エスカレーション
277
+
278
+ ### 実装例
221
279
  ```bash
222
- # 完了報告の確認
223
- ls queue/reports/*.yaml
280
+ # タイムアウト後のスキャン
281
+ echo "⏰ タイムアウト。完了報告をスキャン中..."
282
+ ls queue/reports/*.yaml | grep -v escalation | grep -v completion-summary
283
+
284
+ # 各タスクの完了状態を確認
285
+ for task_id in task-001 task-002 ...; do
286
+ if [ -f "queue/reports/${task_id}-completed.yaml" ]; then
287
+ echo "✅ ${task_id}: 完了済み(通知なしで検出)"
288
+ else
289
+ echo "⏳ ${task_id}: 未完了"
290
+ fi
291
+ done
292
+ ```
224
293
 
225
- # 報告内容の読み取り
226
- cat queue/reports/${TASK_ID}.yaml
294
+ ### 報告の全確認原則(既存ルールを強調)
295
+ Workerから起こされたら、起こした1人だけでなく**全ワーカーの報告ファイルをスキャン**:
296
+ ```bash
297
+ ls queue/reports/*.yaml
227
298
  ```
299
+ 通信ロストした他ワーカーの報告も拾える。
228
300
 
229
301
  ## 完了判定と報告フロー
230
302
 
@@ -232,8 +304,11 @@ cat queue/reports/${TASK_ID}.yaml
232
304
  1. Workerから「タスク${TASK_ID}完了」の通知を受ける
233
305
  2. queue/reports/${TASK_ID}.yaml を確認
234
306
  3. 全タスクの完了を待つ
235
- 4. 結果を集約してstatus/dashboard.mdを更新(Conductorへのsend-keys禁止)
307
+ 4. 結果を集約:
308
+ - status/dashboard.md を「完了」に更新
309
+ - queue/reports/completion-summary.yaml を作成(Conductorがポーリングで検知)
236
310
  5. queue/conductor/dispatch-instruction.yaml を削除(処理済み)
311
+ 6. 「待機中」と表示して次の指示を待つ
237
312
  ```
238
313
 
239
314
  ## 結果集約フォーマット
@@ -340,6 +415,45 @@ worker_id と executed_by が不一致の場合:
340
415
  - [ ] ワーカーが異常終了した
341
416
  - [ ] 指示ファイルのフォーマットが不正
342
417
 
418
+ ## /clear プロトコル(Worker コンテキスト管理)
419
+
420
+ Workerのコンテキスト蓄積を防ぐため、タスク完了後に `/clear` を送信する。
421
+
422
+ ### いつ /clear を送るか
423
+ - **タスク完了報告受信後、次タスク割当前** に送る
424
+ - Worker完了報告 → dashboard更新 → **/clear送信** → 次タスク指示
425
+
426
+ ### /clear 送信手順
427
+ ```bash
428
+ # 1. 次タスクYAMLを先に書き込む(Worker復帰後にすぐ読めるように)
429
+ # queue/tasks/worker-{N}-task.yaml に次タスクを書く
430
+
431
+ # 2. /clear を send-keys で送る
432
+ source .ensemble/panes.env
433
+ tmux send-keys -t "$WORKER_{N}_PANE" '/clear'
434
+ sleep 1
435
+ tmux send-keys -t "$WORKER_{N}_PANE" Enter
436
+
437
+ # 3. Worker復帰を待つ(約5秒)
438
+ sleep 5
439
+
440
+ # 4. タスク読み込み指示を送る
441
+ tmux send-keys -t "$WORKER_{N}_PANE" 'queue/tasks/にタスクがあります。確認して実行してください。'
442
+ sleep 1
443
+ tmux send-keys -t "$WORKER_{N}_PANE" Enter
444
+ ```
445
+
446
+ ### /clear をスキップする場合
447
+ 以下の条件ではスキップ可:
448
+ - 短タスク連続(推定5分以内)
449
+ - 同一ファイル群の連続タスク
450
+ - Workerのコンテキストがまだ軽量(タスク2件目以内)
451
+
452
+ ### Conductor / Dispatch は /clear しない
453
+ - **Dispatch**: 全Worker状態を把握する必要がある
454
+ - **Conductor**: プロジェクト全体像・計画を維持する必要がある
455
+ - コンテキスト逼迫時は `/compact` を自己判断で実行
456
+
343
457
  ## 禁止事項
344
458
 
345
459
  - タスクの内容を判断する
@@ -115,6 +115,31 @@ errors: [] # エラーがあれば記載
115
115
  completed_at: "2026-02-03T10:30:00Z"
116
116
  ```
117
117
 
118
+ ## 完了報告後の通知プロトコル
119
+
120
+ 完了報告ファイル(queue/reports/task-XXX-completed.yaml)を作成した後、
121
+ 必ずDispatchペインに通知せよ:
122
+
123
+ ### 通知手順
124
+ 1. panes.envを読み込む
125
+ 2. DISPATCH_PANEにsend-keysで通知(2回分割)
126
+ 3. フォーマット: 「タスク${TASK_ID}完了。queue/reports/をご確認ください」
127
+
128
+ ### 実装例
129
+ ```bash
130
+ # panes.envを読み込む
131
+ source .ensemble/panes.env
132
+
133
+ # Dispatchに通知(2回分割)
134
+ tmux send-keys -t "$DISPATCH_PANE" 'タスク${TASK_ID}完了。queue/reports/をご確認ください'
135
+ sleep 1
136
+ tmux send-keys -t "$DISPATCH_PANE" Enter
137
+ ```
138
+
139
+ ### 通知失敗時
140
+ - send-keysが失敗してもエラーにしない(Dispatchがポーリングでフォールバック)
141
+ - 完了報告ファイルが最優先(通知は補助)
142
+
118
143
  ## エラー発生時
119
144
 
120
145
  1. エラー内容を完了報告に記載
@@ -172,6 +197,36 @@ completed_at: "2026-02-03T10:30:00Z"
172
197
  4. 報告時に `executed_by: worker-{自分のID}` を必ず記載
173
198
  5. 元のワーカーの報告ファイルに書き込む(ファイル名は変えない)
174
199
 
200
+ ## /clear 後の復帰手順
201
+
202
+ Dispatchから `/clear` を受けた後、以下の手順で最小コストで復帰する。
203
+
204
+ ### 復帰フロー(約3,000トークンで復帰)
205
+ ```
206
+ /clear 実行
207
+
208
+ ▼ CLAUDE.md 自動読み込み
209
+
210
+ ▼ Step 1: 自分のWorker IDを確認
211
+ │ echo $WORKER_ID
212
+ │ → 出力例: 1 → 自分はWorker-1
213
+
214
+ ▼ Step 2: 自分のタスクYAML読み込み
215
+ │ queue/tasks/worker-{N}-task.yaml を読む
216
+ │ → タスクがあれば作業開始
217
+ │ → なければ次の指示を待つ
218
+
219
+ ▼ Step 3: 必要に応じて追加コンテキスト読み込み
220
+ │ タスクYAMLに files フィールドがあれば対象ファイルを読む
221
+
222
+ ▼ 作業開始
223
+ ```
224
+
225
+ ### /clear 復帰の注意事項
226
+ - /clear前のタスクの記憶は消えている。タスクYAMLだけを信頼せよ
227
+ - instructions(エージェント定義)は初回は読まなくてよい(CLAUDE.mdで十分)
228
+ - 2タスク目以降で詳細な手順が必要なら worker.md を読む
229
+
175
230
  ## 禁止事項
176
231
 
177
232
  - 担当外のファイルを編集する