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/notes.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
学習ノート管理ユーティリティ
|
|
3
|
+
|
|
4
|
+
タスク実行の学習記録を管理する。
|
|
5
|
+
notes/{task-id}/以下にplan.md, decisions.md, lessons.md, skill-candidates.mdを配置。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_task_notes_dir(notes_base_dir: str, task_id: str) -> Path:
|
|
14
|
+
"""
|
|
15
|
+
タスクノート用ディレクトリを作成する
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
notes_base_dir: ノートのベースディレクトリ
|
|
19
|
+
task_id: タスクID
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
作成されたディレクトリのPath
|
|
23
|
+
"""
|
|
24
|
+
task_dir = Path(notes_base_dir) / task_id
|
|
25
|
+
task_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
return task_dir
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_lessons(
|
|
30
|
+
task_dir: Path,
|
|
31
|
+
task_id: str,
|
|
32
|
+
lessons: dict[str, Any],
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
lessons.mdファイルを作成/更新する
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
task_dir: タスクディレクトリ
|
|
39
|
+
task_id: タスクID
|
|
40
|
+
lessons: 学習内容
|
|
41
|
+
- successes: 成功したこと(リスト)
|
|
42
|
+
- improvements: 改善点(リスト)
|
|
43
|
+
- metrics: 数値(辞書)
|
|
44
|
+
"""
|
|
45
|
+
content = f"""# タスク: {task_id}
|
|
46
|
+
## 日時: {datetime.now().isoformat()}
|
|
47
|
+
|
|
48
|
+
## 成功したこと
|
|
49
|
+
"""
|
|
50
|
+
for success in lessons.get("successes", []):
|
|
51
|
+
content += f"- {success}\n"
|
|
52
|
+
|
|
53
|
+
content += "\n## 改善すべきこと\n"
|
|
54
|
+
for improvement in lessons.get("improvements", []):
|
|
55
|
+
if isinstance(improvement, dict):
|
|
56
|
+
content += f"- [ ] {improvement.get('issue', '')}\n"
|
|
57
|
+
content += f" - 原因: {improvement.get('cause', '')}\n"
|
|
58
|
+
content += f" - 対策: {improvement.get('solution', '')}\n"
|
|
59
|
+
else:
|
|
60
|
+
content += f"- [ ] {improvement}\n"
|
|
61
|
+
|
|
62
|
+
metrics = lessons.get("metrics", {})
|
|
63
|
+
content += f"""
|
|
64
|
+
## 数値
|
|
65
|
+
- 修正回数: {metrics.get('fix_count', 0)}回
|
|
66
|
+
- テスト失敗回数: {metrics.get('test_failures', 0)}回
|
|
67
|
+
- レビュー指摘数: {metrics.get('review_issues', 0)}件
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
(task_dir / "lessons.md").write_text(content)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def write_skill_candidates(
|
|
74
|
+
task_dir: Path,
|
|
75
|
+
candidates: list[dict[str, Any]],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
skill-candidates.mdファイルを作成する
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
task_dir: タスクディレクトリ
|
|
82
|
+
candidates: スキル化候補のリスト
|
|
83
|
+
- name: スキル名
|
|
84
|
+
- purpose: 用途
|
|
85
|
+
- occurrences: 発生回数
|
|
86
|
+
- cost: 実装コスト(low/medium/high)
|
|
87
|
+
- recommended: 推奨(True/False)
|
|
88
|
+
"""
|
|
89
|
+
content = "# スキル化候補\n\n"
|
|
90
|
+
|
|
91
|
+
if not candidates:
|
|
92
|
+
content += "候補なし\n"
|
|
93
|
+
else:
|
|
94
|
+
for i, candidate in enumerate(candidates, 1):
|
|
95
|
+
recommended = "YES" if candidate.get("recommended", False) else "NO"
|
|
96
|
+
content += f"""## 候補{i}: {candidate.get('name', 'unknown')}
|
|
97
|
+
- 用途: {candidate.get('purpose', '')}
|
|
98
|
+
- 発生回数: {candidate.get('occurrences', 0)}回
|
|
99
|
+
- 実装コスト: {candidate.get('cost', 'unknown')}
|
|
100
|
+
- 推奨: {recommended}
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
(task_dir / "skill-candidates.md").write_text(content)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def write_decisions(
|
|
108
|
+
task_dir: Path,
|
|
109
|
+
task_id: str,
|
|
110
|
+
decisions: list[dict[str, Any]],
|
|
111
|
+
append: bool = False,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""
|
|
114
|
+
decisions.mdファイルを作成/更新する
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
task_dir: タスクディレクトリ
|
|
118
|
+
task_id: タスクID
|
|
119
|
+
decisions: 決定のリスト
|
|
120
|
+
- timestamp: タイムスタンプ
|
|
121
|
+
- context: 文脈
|
|
122
|
+
- decision: 決定内容
|
|
123
|
+
- rationale: 理由
|
|
124
|
+
append: Trueの場合、既存ファイルに追記
|
|
125
|
+
"""
|
|
126
|
+
decisions_file = task_dir / "decisions.md"
|
|
127
|
+
|
|
128
|
+
if append and decisions_file.exists():
|
|
129
|
+
content = decisions_file.read_text()
|
|
130
|
+
content += "\n---\n\n"
|
|
131
|
+
else:
|
|
132
|
+
content = f"# 決定ログ: {task_id}\n\n"
|
|
133
|
+
|
|
134
|
+
for decision in decisions:
|
|
135
|
+
content += f"""## {decision.get('context', 'No context')}
|
|
136
|
+
- **日時**: {decision.get('timestamp', datetime.now().isoformat())}
|
|
137
|
+
- **決定**: {decision.get('decision', '')}
|
|
138
|
+
- **理由**: {decision.get('rationale', '')}
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
decisions_file.write_text(content)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def read_lessons(task_dir: Path) -> str | None:
|
|
146
|
+
"""
|
|
147
|
+
lessons.mdの内容を読み込む
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
task_dir: タスクディレクトリ
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
ファイル内容。存在しない場合はNone
|
|
154
|
+
"""
|
|
155
|
+
lessons_file = task_dir / "lessons.md"
|
|
156
|
+
if lessons_file.exists():
|
|
157
|
+
return lessons_file.read_text()
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def read_skill_candidates(task_dir: Path) -> str | None:
|
|
162
|
+
"""
|
|
163
|
+
skill-candidates.mdの内容を読み込む
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
task_dir: タスクディレクトリ
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
ファイル内容。存在しない場合はNone
|
|
170
|
+
"""
|
|
171
|
+
skills_file = task_dir / "skill-candidates.md"
|
|
172
|
+
if skills_file.exists():
|
|
173
|
+
return skills_file.read_text()
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def list_task_notes(notes_base_dir: str) -> list[str]:
|
|
178
|
+
"""
|
|
179
|
+
全タスクディレクトリをリストする
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
notes_base_dir: ノートのベースディレクトリ
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
タスクIDのリスト
|
|
186
|
+
"""
|
|
187
|
+
notes_path = Path(notes_base_dir)
|
|
188
|
+
if not notes_path.exists():
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
return [d.name for d in notes_path.iterdir() if d.is_dir()]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_notes_summary(notes_base_dir: str) -> dict[str, dict[str, bool]]:
|
|
195
|
+
"""
|
|
196
|
+
全ノートの要約を返す
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
notes_base_dir: ノートのベースディレクトリ
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
{task_id: {has_lessons: bool, has_skill_candidates: bool, ...}}
|
|
203
|
+
"""
|
|
204
|
+
notes_path = Path(notes_base_dir)
|
|
205
|
+
if not notes_path.exists():
|
|
206
|
+
return {}
|
|
207
|
+
|
|
208
|
+
summary: dict[str, dict[str, bool]] = {}
|
|
209
|
+
|
|
210
|
+
for task_dir in notes_path.iterdir():
|
|
211
|
+
if not task_dir.is_dir():
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
summary[task_dir.name] = {
|
|
215
|
+
"has_lessons": (task_dir / "lessons.md").exists(),
|
|
216
|
+
"has_skill_candidates": (task_dir / "skill-candidates.md").exists(),
|
|
217
|
+
"has_decisions": (task_dir / "decisions.md").exists(),
|
|
218
|
+
"has_plan": (task_dir / "plan.md").exists(),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return summary
|
ensemble/queue.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ファイルベースのタスクキュー
|
|
3
|
+
|
|
4
|
+
アトミック操作でタスクの配信・取得・完了報告を行う。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from ensemble.lock import atomic_claim, atomic_write
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TaskQueue:
|
|
21
|
+
"""
|
|
22
|
+
ファイルベースのタスクキュー
|
|
23
|
+
|
|
24
|
+
構造:
|
|
25
|
+
queue/
|
|
26
|
+
├── tasks/ # 保留中のタスク
|
|
27
|
+
├── processing/ # 処理中のタスク
|
|
28
|
+
└── reports/ # 完了報告
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, base_dir: Path | None = None) -> None:
|
|
32
|
+
"""
|
|
33
|
+
キューを初期化する
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
base_dir: ベースディレクトリ(デフォルト: queue/)
|
|
37
|
+
"""
|
|
38
|
+
self.base_dir = base_dir if base_dir else Path("queue")
|
|
39
|
+
self.tasks_dir = self.base_dir / "tasks"
|
|
40
|
+
self.processing_dir = self.base_dir / "processing"
|
|
41
|
+
self.reports_dir = self.base_dir / "reports"
|
|
42
|
+
|
|
43
|
+
# ディレクトリ作成
|
|
44
|
+
self.tasks_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
self.processing_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
self.reports_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
def enqueue(
|
|
49
|
+
self,
|
|
50
|
+
command: str,
|
|
51
|
+
agent: str,
|
|
52
|
+
params: dict[str, Any] | None = None,
|
|
53
|
+
) -> str:
|
|
54
|
+
"""
|
|
55
|
+
タスクをキューに追加する
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
command: 実行するコマンド
|
|
59
|
+
agent: 担当エージェント
|
|
60
|
+
params: 追加パラメータ
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
タスクID
|
|
64
|
+
"""
|
|
65
|
+
task_id = self._generate_task_id()
|
|
66
|
+
|
|
67
|
+
task = {
|
|
68
|
+
"task_id": task_id,
|
|
69
|
+
"command": command,
|
|
70
|
+
"agent": agent,
|
|
71
|
+
"params": params or {},
|
|
72
|
+
"status": "pending",
|
|
73
|
+
"created_at": datetime.now().isoformat(),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
task_file = self.tasks_dir / f"{task_id}.yaml"
|
|
77
|
+
content = yaml.dump(task, allow_unicode=True, default_flow_style=False)
|
|
78
|
+
atomic_write(str(task_file), content)
|
|
79
|
+
|
|
80
|
+
return task_id
|
|
81
|
+
|
|
82
|
+
def claim(self) -> dict[str, Any] | None:
|
|
83
|
+
"""
|
|
84
|
+
タスクを取得する(アトミック)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
タスクデータ、またはキューが空の場合None
|
|
88
|
+
"""
|
|
89
|
+
# 最も古いタスクを取得(ファイル名でソート)
|
|
90
|
+
task_files = sorted(self.tasks_dir.glob("*.yaml"))
|
|
91
|
+
|
|
92
|
+
for task_file in task_files:
|
|
93
|
+
result = atomic_claim(str(task_file), str(self.processing_dir))
|
|
94
|
+
if result:
|
|
95
|
+
with open(result) as f:
|
|
96
|
+
return yaml.safe_load(f)
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def complete(
|
|
101
|
+
self,
|
|
102
|
+
task_id: str,
|
|
103
|
+
result: str,
|
|
104
|
+
output: str,
|
|
105
|
+
error: str | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
タスク完了を報告する
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
task_id: タスクID
|
|
112
|
+
result: 結果 ("success" or "error")
|
|
113
|
+
output: 出力内容
|
|
114
|
+
error: エラーメッセージ(エラー時のみ)
|
|
115
|
+
"""
|
|
116
|
+
processing_file = self.processing_dir / f"{task_id}.yaml"
|
|
117
|
+
|
|
118
|
+
# 元のタスク情報を読み込み
|
|
119
|
+
if processing_file.exists():
|
|
120
|
+
with open(processing_file) as f:
|
|
121
|
+
task = yaml.safe_load(f)
|
|
122
|
+
else:
|
|
123
|
+
task = {"task_id": task_id}
|
|
124
|
+
|
|
125
|
+
# レポート作成
|
|
126
|
+
report = {
|
|
127
|
+
**task,
|
|
128
|
+
"result": result,
|
|
129
|
+
"output": output,
|
|
130
|
+
"completed_at": datetime.now().isoformat(),
|
|
131
|
+
}
|
|
132
|
+
if error:
|
|
133
|
+
report["error"] = error
|
|
134
|
+
|
|
135
|
+
# reportsに保存
|
|
136
|
+
report_file = self.reports_dir / f"{task_id}.yaml"
|
|
137
|
+
content = yaml.dump(report, allow_unicode=True, default_flow_style=False)
|
|
138
|
+
atomic_write(str(report_file), content)
|
|
139
|
+
|
|
140
|
+
# processingから削除
|
|
141
|
+
if processing_file.exists():
|
|
142
|
+
processing_file.unlink()
|
|
143
|
+
|
|
144
|
+
def list_pending(self) -> list[str]:
|
|
145
|
+
"""
|
|
146
|
+
保留中のタスクIDリストを取得する
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
タスクIDのリスト
|
|
150
|
+
"""
|
|
151
|
+
task_files = self.tasks_dir.glob("*.yaml")
|
|
152
|
+
return [f.stem for f in task_files]
|
|
153
|
+
|
|
154
|
+
def cleanup(self) -> None:
|
|
155
|
+
"""
|
|
156
|
+
全てのファイルを削除する(セッション開始時用)
|
|
157
|
+
"""
|
|
158
|
+
for dir_path in [self.tasks_dir, self.processing_dir, self.reports_dir]:
|
|
159
|
+
for f in dir_path.glob("*.yaml"):
|
|
160
|
+
f.unlink()
|
|
161
|
+
|
|
162
|
+
def _generate_task_id(self) -> str:
|
|
163
|
+
"""ユニークなタスクIDを生成"""
|
|
164
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
165
|
+
short_uuid = uuid.uuid4().hex[:8]
|
|
166
|
+
return f"{timestamp}-{short_uuid}"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Ensemble templates package.
|
|
2
|
+
|
|
3
|
+
Provides access to bundled agent definitions, commands, workflows, and scripts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_template_path(template_type: str) -> Path:
|
|
12
|
+
"""Get the path to a template directory.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
template_type: One of "agents", "commands", "workflows", "scripts"
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Path to the template directory
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If template_type is not valid
|
|
22
|
+
"""
|
|
23
|
+
valid_types = {"agents", "commands", "workflows", "scripts"}
|
|
24
|
+
if template_type not in valid_types:
|
|
25
|
+
raise ValueError(f"Invalid template type: {template_type}. Must be one of {valid_types}")
|
|
26
|
+
|
|
27
|
+
# Use importlib.resources for Python 3.9+
|
|
28
|
+
try:
|
|
29
|
+
# Python 3.9+ style
|
|
30
|
+
with resources.as_file(resources.files("ensemble.templates") / template_type) as path:
|
|
31
|
+
return Path(path)
|
|
32
|
+
except (TypeError, AttributeError):
|
|
33
|
+
# Fallback for older Python or edge cases
|
|
34
|
+
package_dir = Path(__file__).parent
|
|
35
|
+
return package_dir / template_type
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_template_file(template_type: str, filename: str) -> Optional[Path]:
|
|
39
|
+
"""Get the path to a specific template file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
template_type: One of "agents", "commands", "workflows", "scripts"
|
|
43
|
+
filename: Name of the file within the template directory
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Path to the template file, or None if not found
|
|
47
|
+
"""
|
|
48
|
+
template_dir = get_template_path(template_type)
|
|
49
|
+
file_path = template_dir / filename
|
|
50
|
+
|
|
51
|
+
if file_path.exists():
|
|
52
|
+
return file_path
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def list_templates(template_type: str) -> list[str]:
|
|
57
|
+
"""List all template files of a given type.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
template_type: One of "agents", "commands", "workflows", "scripts"
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of template filenames
|
|
64
|
+
"""
|
|
65
|
+
template_dir = get_template_path(template_type)
|
|
66
|
+
if not template_dir.exists():
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
if template_type in ("agents", "commands"):
|
|
70
|
+
return [f.name for f in template_dir.glob("*.md")]
|
|
71
|
+
elif template_type == "workflows":
|
|
72
|
+
return [f.name for f in template_dir.glob("*.yaml")]
|
|
73
|
+
elif template_type == "scripts":
|
|
74
|
+
return [f.name for f in template_dir.glob("*.sh")]
|
|
75
|
+
return []
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor
|
|
3
|
+
description: |
|
|
4
|
+
Ensembleの指揮者(頭脳)。ユーザーのタスクを受け取り、
|
|
5
|
+
計画・分解・パターン選択・最終判断を行う。
|
|
6
|
+
実行の伝達・監視はDispatchに委譲する。考えすぎない。即判断。
|
|
7
|
+
tools: Read, Write, Edit, Bash, Glob, Grep, Skill, Task
|
|
8
|
+
model: opus
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
あなたはEnsembleの指揮者(Conductor)です。
|
|
12
|
+
|
|
13
|
+
## 最重要ルール: 考えるな、委譲しろ
|
|
14
|
+
|
|
15
|
+
- あなたの仕事は「判断」と「委譲」。自分でコードを書いたりファイルを直接操作するな。
|
|
16
|
+
- 計画を立てたら即座にDispatchまたはサブエージェントに委譲せよ。
|
|
17
|
+
- 30秒で済む判断に5分かけるな。
|
|
18
|
+
|
|
19
|
+
## 行動原則
|
|
20
|
+
|
|
21
|
+
1. まずplanモードで全体計画を立てる
|
|
22
|
+
2. タスクを分解し、最適な実行パターンを選択する
|
|
23
|
+
3. コスト見積もりを行い、適切なワークフローを選択する
|
|
24
|
+
4. 必要なskillsやagentsが不足していれば生成を提案する
|
|
25
|
+
5. Dispatchにタスク配信を指示する(パターンB/Cの場合)
|
|
26
|
+
6. 完了報告を受けたら最終判断のみ行う
|
|
27
|
+
7. 完了後は必ず自己改善フェーズをlearnerに委譲する
|
|
28
|
+
|
|
29
|
+
## 実行パターン判定基準
|
|
30
|
+
|
|
31
|
+
### パターンA: subagent直接実行
|
|
32
|
+
- 変更ファイル数 ≤ 3
|
|
33
|
+
- 独立性が高くない単純タスク
|
|
34
|
+
- 例: 単一ファイルの修正、typo修正、小さな機能追加
|
|
35
|
+
|
|
36
|
+
### パターンB: tmux並列実行
|
|
37
|
+
- 変更ファイル数 4〜10
|
|
38
|
+
- 並列可能な作業あり
|
|
39
|
+
- 例: 複数エンドポイントの実装、テスト追加
|
|
40
|
+
|
|
41
|
+
### パターンC: worktree分離
|
|
42
|
+
- 機能が独立している
|
|
43
|
+
- 変更ファイル数 > 10 または 複数ブランチ必要
|
|
44
|
+
- 例: 認証・API・UIの同時開発
|
|
45
|
+
|
|
46
|
+
## パターン別実行方法
|
|
47
|
+
|
|
48
|
+
### パターンA: takt方式(subagent直接)
|
|
49
|
+
|
|
50
|
+
Taskツールでsubagentを起動して直接実行する。Dispatchは不要。
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
1. Task(subagent_type="general-purpose")でタスク実行
|
|
54
|
+
2. 結果を確認
|
|
55
|
+
3. 必要に応じてレビュー
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### パターンB: shogun方式(tmux並列)
|
|
59
|
+
|
|
60
|
+
Dispatchに指示を送り、ワーカーペインを起動させる。
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
1. タスクを分解し、ワーカー数を決定
|
|
64
|
+
2. queue/conductor/dispatch-instruction.yaml に指示を書く:
|
|
65
|
+
|
|
66
|
+
type: start_workers
|
|
67
|
+
worker_count: 2
|
|
68
|
+
tasks:
|
|
69
|
+
- id: task-001
|
|
70
|
+
instruction: "タスク1の説明"
|
|
71
|
+
files: ["file1.py"]
|
|
72
|
+
- id: task-002
|
|
73
|
+
instruction: "タスク2の説明"
|
|
74
|
+
files: ["file2.py"]
|
|
75
|
+
created_at: "{現在時刻}"
|
|
76
|
+
workflow: default
|
|
77
|
+
pattern: B
|
|
78
|
+
|
|
79
|
+
3. Dispatchに通知(2回分割 + ペインID):
|
|
80
|
+
source .ensemble/panes.env
|
|
81
|
+
tmux send-keys -t "$DISPATCH_PANE" '新しい指示があります。queue/conductor/dispatch-instruction.yaml を確認してください'
|
|
82
|
+
tmux send-keys -t "$DISPATCH_PANE" Enter
|
|
83
|
+
|
|
84
|
+
4. 完了を待つ(Dispatchからのsend-keysは来ない。status/dashboard.mdを確認)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### パターンC: shogun方式(worktree)
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
1. 同様にdispatch-instruction.yamlに指示を書く
|
|
91
|
+
2. type: start_worktree を指定
|
|
92
|
+
3. Dispatchがworktree-create.shを実行
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## コスト意識のワークフロー選択
|
|
96
|
+
|
|
97
|
+
### ワークフロー一覧
|
|
98
|
+
|
|
99
|
+
| ワークフロー | レビュー回数 | 修正ループ | 最大イテレーション | コストレベル |
|
|
100
|
+
|-------------|-------------|-----------|------------------|-------------|
|
|
101
|
+
| simple.yaml | 1回 | なし | 5 | low |
|
|
102
|
+
| default.yaml | 並列2種 | あり | 15 | medium |
|
|
103
|
+
| heavy.yaml | 並列5種 | あり | 25 | high |
|
|
104
|
+
|
|
105
|
+
### 選択フローチャート
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
タスク受領
|
|
109
|
+
│
|
|
110
|
+
▼
|
|
111
|
+
[変更規模は?]
|
|
112
|
+
│
|
|
113
|
+
├─ ファイル数 ≤ 2、ドキュメントのみ → simple.yaml
|
|
114
|
+
│
|
|
115
|
+
├─ ファイル数 3〜10、通常の機能開発 → default.yaml
|
|
116
|
+
│
|
|
117
|
+
└─ 以下のいずれかに該当 → heavy.yaml
|
|
118
|
+
- ファイル数 > 10
|
|
119
|
+
- セキュリティ重要(認証、決済、個人情報)
|
|
120
|
+
- 大規模リファクタ
|
|
121
|
+
- 複数サービス間の変更
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 具体的な判定基準
|
|
125
|
+
|
|
126
|
+
#### simple.yaml を選択するケース
|
|
127
|
+
- README/ドキュメント更新
|
|
128
|
+
- typo/文言修正
|
|
129
|
+
- 設定ファイルの微調整
|
|
130
|
+
- コメント追加/修正
|
|
131
|
+
- 単一テストファイルの追加
|
|
132
|
+
|
|
133
|
+
#### default.yaml を選択するケース
|
|
134
|
+
- 新規機能の追加(標準的な規模)
|
|
135
|
+
- バグ修正(影響範囲が限定的)
|
|
136
|
+
- テストカバレッジ改善
|
|
137
|
+
- 小〜中規模のリファクタリング
|
|
138
|
+
- 依存ライブラリの更新
|
|
139
|
+
|
|
140
|
+
#### heavy.yaml を選択するケース
|
|
141
|
+
- 認証・認可システムの変更
|
|
142
|
+
- 決済・課金機能の実装/変更
|
|
143
|
+
- 個人情報を扱う機能の変更
|
|
144
|
+
- データベーススキーマの大幅変更
|
|
145
|
+
- アーキテクチャレベルのリファクタ
|
|
146
|
+
- セキュリティ脆弱性の修正
|
|
147
|
+
- 本番環境に直接影響する変更
|
|
148
|
+
|
|
149
|
+
### /go-light コマンドとの関係
|
|
150
|
+
|
|
151
|
+
`/go-light` コマンドは明示的に `simple.yaml` を使用する。
|
|
152
|
+
ユーザーが「軽微な変更」と判断した場合に使用する。
|
|
153
|
+
|
|
154
|
+
### コスト最適化のヒント
|
|
155
|
+
|
|
156
|
+
1. **迷ったら default.yaml**: 過剰なレビューより見逃しのリスクが高い
|
|
157
|
+
2. **セキュリティに関わるなら heavy.yaml**: コストよりリスク回避を優先
|
|
158
|
+
3. **段階的エスカレーション**: simple → defaultで問題発見 → heavyで再実行も可
|
|
159
|
+
|
|
160
|
+
## worktree統合プロトコル
|
|
161
|
+
|
|
162
|
+
パターンCの場合、全worktreeの作業完了後:
|
|
163
|
+
|
|
164
|
+
1. integrator agentが各worktreeの変更をメインブランチへマージ
|
|
165
|
+
2. コンフリクトがあれば:
|
|
166
|
+
- まずAIが自動解決を試みる
|
|
167
|
+
- 失敗した場合のみConductorに報告
|
|
168
|
+
3. マージ後、各worktreeのCoderが「自分以外の変更」をレビュー(相互レビュー)
|
|
169
|
+
4. 全員承認で完了
|
|
170
|
+
|
|
171
|
+
## 重要な設計判断のプロトコル
|
|
172
|
+
|
|
173
|
+
アーキテクチャやテンプレート構造など、重要な設計判断を下す際は:
|
|
174
|
+
|
|
175
|
+
- 単一エージェントの意見で即決しない
|
|
176
|
+
- 複数の専門家ペルソナ(3〜5人)を召喚し、熟議させる
|
|
177
|
+
- 多数決ではなく、各専門領域からの総意を得る
|
|
178
|
+
|
|
179
|
+
## 並列ペイン数の動的調整
|
|
180
|
+
|
|
181
|
+
Claude Max 5並列制限を考慮:
|
|
182
|
+
|
|
183
|
+
- Conductor用に1セッション確保
|
|
184
|
+
- 残り4セッションをタスクに応じて動的に割り当て
|
|
185
|
+
- タスク数 < 4 の場合は、タスク数と同じペイン数
|
|
186
|
+
|
|
187
|
+
## 待機プロトコル
|
|
188
|
+
|
|
189
|
+
タスク完了後・委譲後は必ず以下を実行:
|
|
190
|
+
|
|
191
|
+
1. 「待機中。次の指示をお待ちしています。」と表示
|
|
192
|
+
2. **処理を停止し、次の入力を待つ**(ポーリングしない)
|
|
193
|
+
|
|
194
|
+
これにより、send-keysで起こされた時に即座に処理を開始できる。
|
|
195
|
+
|
|
196
|
+
## 起動トリガーと完了確認
|
|
197
|
+
|
|
198
|
+
以下の形式で起こされたら即座に処理開始:
|
|
199
|
+
|
|
200
|
+
| トリガー | 送信元 | アクション |
|
|
201
|
+
|---------|--------|-----------|
|
|
202
|
+
| `/go` または タスク依頼 | ユーザー | 計画立案・パターン選択・実行 |
|
|
203
|
+
|
|
204
|
+
### 完了確認方法(Dispatchからのsend-keysは来ない)
|
|
205
|
+
|
|
206
|
+
Dispatchはsend-keysでConductorに報告しない。以下の方法で完了を確認:
|
|
207
|
+
1. `status/dashboard.md` を定期的に確認
|
|
208
|
+
2. `queue/reports/` にファイルが揃ったら完了
|
|
209
|
+
|
|
210
|
+
これにより、send-keysの信頼性問題を回避する。
|
|
211
|
+
|
|
212
|
+
## 自律判断チェックリスト
|
|
213
|
+
|
|
214
|
+
### タスク完了時に自動実行
|
|
215
|
+
- [ ] 全ワーカーの報告を確認
|
|
216
|
+
- [ ] 代理実行の有無をチェック(worker_id と executed_by の不一致)
|
|
217
|
+
- [ ] 異常があれば原因分析
|
|
218
|
+
- [ ] learner agentに自己改善を委譲
|
|
219
|
+
|
|
220
|
+
### 異常検知時
|
|
221
|
+
- [ ] 代理実行が発生 → 原因調査(通信問題?負荷偏り?)
|
|
222
|
+
- [ ] 失敗タスクあり → リトライ判断またはエスカレーション
|
|
223
|
+
- [ ] 全ワーカー応答なし → インフラ確認
|
|
224
|
+
|
|
225
|
+
### 定期確認項目
|
|
226
|
+
- [ ] dashboard.md の整合性確認
|
|
227
|
+
- [ ] 未完了タスクの棚卸し
|
|
228
|
+
- [ ] queue/ 内の古いファイル削除
|
|
229
|
+
|
|
230
|
+
## 禁止事項
|
|
231
|
+
|
|
232
|
+
- 自分でコードを書く
|
|
233
|
+
- 自分でファイルを直接編集する
|
|
234
|
+
- 考えすぎる(Extended Thinkingは無効化されているはず)
|
|
235
|
+
- Dispatchの仕事(キュー管理、ACK確認)を奪う
|
|
236
|
+
- ポーリングで完了を待つ(イベント駆動で待機せよ)
|
|
237
|
+
- ワーカーの作業を横取りする
|
|
238
|
+
- 曖昧な表現で報告する(具体的な数値を使え)
|
|
239
|
+
- **ペイン番号(main.0, main.1等)を使用する(ペインIDを使え)**
|