@hummer98/cmux-team 3.0.0

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 (51) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/.claude-plugin/plugin.json +15 -0
  3. package/CHANGELOG.md +279 -0
  4. package/LICENSE +21 -0
  5. package/README.ja.md +238 -0
  6. package/README.md +158 -0
  7. package/bin/cmux-team.js +29 -0
  8. package/bin/postinstall.js +26 -0
  9. package/commands/master.md +8 -0
  10. package/commands/start.md +42 -0
  11. package/commands/team-archive.md +63 -0
  12. package/commands/team-design.md +199 -0
  13. package/commands/team-disband.md +79 -0
  14. package/commands/team-impl.md +201 -0
  15. package/commands/team-research.md +182 -0
  16. package/commands/team-review.md +222 -0
  17. package/commands/team-spec.md +133 -0
  18. package/commands/team-status.md +24 -0
  19. package/commands/team-sync-docs.md +127 -0
  20. package/commands/team-task.md +158 -0
  21. package/commands/team-test.md +230 -0
  22. package/package.json +51 -0
  23. package/skills/cmux-agent-role/SKILL.md +166 -0
  24. package/skills/cmux-team/SKILL.md +568 -0
  25. package/skills/cmux-team/manager/bun.lock +118 -0
  26. package/skills/cmux-team/manager/cmux.ts +134 -0
  27. package/skills/cmux-team/manager/conductor.ts +347 -0
  28. package/skills/cmux-team/manager/daemon.ts +373 -0
  29. package/skills/cmux-team/manager/e2e.ts +550 -0
  30. package/skills/cmux-team/manager/logger.ts +13 -0
  31. package/skills/cmux-team/manager/main.ts +756 -0
  32. package/skills/cmux-team/manager/master.ts +51 -0
  33. package/skills/cmux-team/manager/package.json +18 -0
  34. package/skills/cmux-team/manager/proxy.ts +219 -0
  35. package/skills/cmux-team/manager/queue.ts +73 -0
  36. package/skills/cmux-team/manager/schema.ts +84 -0
  37. package/skills/cmux-team/manager/task.ts +160 -0
  38. package/skills/cmux-team/manager/template.ts +92 -0
  39. package/skills/cmux-team/templates/architect.md +25 -0
  40. package/skills/cmux-team/templates/common-header.md +12 -0
  41. package/skills/cmux-team/templates/conductor-role.md +244 -0
  42. package/skills/cmux-team/templates/conductor-task.md +37 -0
  43. package/skills/cmux-team/templates/conductor.md +275 -0
  44. package/skills/cmux-team/templates/dockeeper.md +23 -0
  45. package/skills/cmux-team/templates/implementer.md +24 -0
  46. package/skills/cmux-team/templates/manager.md +199 -0
  47. package/skills/cmux-team/templates/master.md +94 -0
  48. package/skills/cmux-team/templates/researcher.md +24 -0
  49. package/skills/cmux-team/templates/reviewer.md +28 -0
  50. package/skills/cmux-team/templates/task-manager.md +22 -0
  51. package/skills/cmux-team/templates/tester.md +27 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Master surface の作成・管理
3
+ */
4
+ import * as cmux from "./cmux";
5
+ import { generateMasterPrompt } from "./template";
6
+ import { log } from "./logger";
7
+
8
+ export interface MasterState {
9
+ surface: string;
10
+ }
11
+
12
+ export async function spawnMaster(
13
+ projectRoot: string,
14
+ daemonSurface?: string
15
+ ): Promise<MasterState | null> {
16
+ try {
17
+ // プロンプト生成
18
+ await generateMasterPrompt(projectRoot);
19
+
20
+ // ペイン作成(daemon surface を右に split)
21
+ const surface = await cmux.newSplit("right", daemonSurface ? { surface: daemonSurface } : undefined);
22
+
23
+ if (!(await cmux.validateSurface(surface))) {
24
+ await log("error", `Master surface ${surface} validation failed`);
25
+ return null;
26
+ }
27
+
28
+ // Claude Code 起動
29
+ await cmux.send(
30
+ surface,
31
+ "claude --dangerously-skip-permissions --append-system-prompt-file .team/prompts/master.md 'ユーザーからのタスクを待ってください。'\n"
32
+ );
33
+
34
+ // Trust 承認
35
+ await cmux.waitForTrust(surface);
36
+
37
+ // タブ名設定
38
+ const num = surface.replace("surface:", "");
39
+ await cmux.renameTab(surface, `[${num}] Master`);
40
+
41
+ await log("master_spawned", `surface=${surface}`);
42
+ return { surface };
43
+ } catch (e: any) {
44
+ await log("error", `Master spawn failed: ${e.message}`);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export async function isMasterAlive(surface: string): Promise<boolean> {
50
+ return cmux.validateSurface(surface);
51
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "manager",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "private": true,
6
+ "devDependencies": {
7
+ "@types/bun": "latest",
8
+ "@types/react": "^19.2.14"
9
+ },
10
+ "peerDependencies": {
11
+ "typescript": "^5"
12
+ },
13
+ "dependencies": {
14
+ "ink": "^6.8.0",
15
+ "react": "^19.2.4",
16
+ "zod": "^4.3.6"
17
+ }
18
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * ロギングプロキシ — Anthropic API への透過プロキシ + JSONL トレース
3
+ *
4
+ * Agent の ANTHROPIC_BASE_URL をこのプロキシに向けることで、
5
+ * リクエスト/レスポンスを .team/logs/traces/ に記録する。
6
+ * レスポンスは streaming のまま返し、ログは非同期で書き込む。
7
+ */
8
+ import { mkdir, appendFile, readFile } from "fs/promises";
9
+ import { join } from "path";
10
+
11
+ const DEFAULT_UPSTREAM = "https://api.anthropic.com";
12
+
13
+ interface ProxyHandle {
14
+ port: number;
15
+ stop: () => void;
16
+ }
17
+
18
+ interface TraceEntry {
19
+ timestamp: string;
20
+ conductor_id?: string;
21
+ task_id?: string;
22
+ role?: string;
23
+ method: string;
24
+ path: string;
25
+ status?: number;
26
+ request_bytes: number;
27
+ response_bytes: number;
28
+ duration_ms: number;
29
+ }
30
+
31
+ export async function start(
32
+ projectRoot: string,
33
+ opts?: { conductorId?: string; taskId?: string; role?: string; getState?: () => any }
34
+ ): Promise<ProxyHandle> {
35
+ const upstream = process.env.ANTHROPIC_API_URL || DEFAULT_UPSTREAM;
36
+ const tracesDir = join(projectRoot, ".team/logs/traces");
37
+ await mkdir(tracesDir, { recursive: true });
38
+
39
+ const traceFile = join(tracesDir, "api-trace.jsonl");
40
+
41
+ // 前回ポートの読み取り(daemon リロード時に同じポートを再利用)
42
+ let preferredPort = 0;
43
+ try {
44
+ const saved = await readFile(join(projectRoot, ".team/proxy-port"), "utf-8");
45
+ const parsed = parseInt(saved.trim(), 10);
46
+ if (parsed > 0) preferredPort = parsed;
47
+ } catch {
48
+ // ファイルなし(初回起動)→ ランダムポート
49
+ }
50
+
51
+ const fetchHandler = async (req: Request) => {
52
+ const url = new URL(req.url);
53
+
54
+ // デバッグエンドポイント
55
+ if (req.method === "GET") {
56
+ const jsonHeaders = { "Content-Type": "application/json" };
57
+
58
+ if (url.pathname === "/state") {
59
+ if (!opts?.getState) return new Response("Not Found", { status: 404 });
60
+ const state = opts.getState();
61
+ const serialized = {
62
+ ...state,
63
+ conductors: Object.fromEntries(state.conductors),
64
+ lastUpdate: state.lastUpdate instanceof Date ? state.lastUpdate.toISOString() : state.lastUpdate,
65
+ };
66
+ return new Response(JSON.stringify(serialized), { headers: jsonHeaders });
67
+ }
68
+
69
+ if (url.pathname === "/tasks") {
70
+ if (!opts?.getState) return new Response("Not Found", { status: 404 });
71
+ const state = opts.getState();
72
+ return new Response(JSON.stringify(state.taskList), { headers: jsonHeaders });
73
+ }
74
+
75
+ if (url.pathname === "/conductors") {
76
+ if (!opts?.getState) return new Response("Not Found", { status: 404 });
77
+ const state = opts.getState();
78
+ return new Response(JSON.stringify(Object.fromEntries(state.conductors)), { headers: jsonHeaders });
79
+ }
80
+ }
81
+ const targetUrl = `${upstream}${url.pathname}${url.search}`;
82
+ const startTime = Date.now();
83
+
84
+ // リクエストボディを読み取り(転送用 + サイズ計測用)
85
+ const reqBody = req.body ? await req.arrayBuffer() : null;
86
+ const requestBytes = reqBody?.byteLength ?? 0;
87
+
88
+ // Host ヘッダーを除外して転送(そのまま渡すと Bun が
89
+ // Host の値を接続先に使い、プロキシ自身に接続してしまう)
90
+ const fwdHeaders = new Headers(req.headers);
91
+ fwdHeaders.delete("host");
92
+ fwdHeaders.delete("accept-encoding");
93
+
94
+ const upstreamRes = await fetch(targetUrl, {
95
+ method: req.method,
96
+ headers: fwdHeaders,
97
+ body: reqBody,
98
+ });
99
+
100
+ // Bun の fetch は自動解凍するので Content-Encoding / Content-Length を除去
101
+ const resHeaders = new Headers(upstreamRes.headers);
102
+ resHeaders.delete("content-encoding");
103
+ resHeaders.delete("content-length");
104
+
105
+ // レスポンスが streaming かどうかを判定
106
+ const contentType = upstreamRes.headers.get("content-type") || "";
107
+ const isStreaming = contentType.includes("text/event-stream");
108
+
109
+ if (isStreaming && upstreamRes.body) {
110
+ // streaming: tee して片方をログに使う
111
+ const [clientStream, logStream] = upstreamRes.body.tee();
112
+
113
+ // 非同期でログ書き込み(レスポンスはブロックしない)
114
+ drainAndLog(logStream, {
115
+ tracesDir: traceFile,
116
+ method: req.method,
117
+ path: url.pathname,
118
+ status: upstreamRes.status,
119
+ requestBytes,
120
+ startTime,
121
+ conductorId: opts?.conductorId,
122
+ taskId: opts?.taskId,
123
+ role: opts?.role,
124
+ });
125
+
126
+ return new Response(clientStream, {
127
+ status: upstreamRes.status,
128
+ statusText: upstreamRes.statusText,
129
+ headers: resHeaders,
130
+ });
131
+ }
132
+
133
+ // 非 streaming: ボディ全体を取得してログ
134
+ const resBody = await upstreamRes.arrayBuffer();
135
+ const duration = Date.now() - startTime;
136
+
137
+ const entry: TraceEntry = {
138
+ timestamp: new Date().toISOString(),
139
+ conductor_id: opts?.conductorId,
140
+ task_id: opts?.taskId,
141
+ role: opts?.role,
142
+ method: req.method,
143
+ path: url.pathname,
144
+ status: upstreamRes.status,
145
+ request_bytes: requestBytes,
146
+ response_bytes: resBody.byteLength,
147
+ duration_ms: duration,
148
+ };
149
+
150
+ // 非同期でログ書き込み
151
+ appendFile(traceFile, JSON.stringify(entry) + "\n").catch(() => {});
152
+
153
+ return new Response(resBody, {
154
+ status: upstreamRes.status,
155
+ statusText: upstreamRes.statusText,
156
+ headers: resHeaders,
157
+ });
158
+ };
159
+
160
+ // 前回ポートで起動を試み、失敗時はランダムポートにフォールバック
161
+ let server: ReturnType<typeof Bun.serve>;
162
+ try {
163
+ server = Bun.serve({ port: preferredPort, fetch: fetchHandler });
164
+ } catch {
165
+ if (preferredPort !== 0) {
166
+ server = Bun.serve({ port: 0, fetch: fetchHandler });
167
+ } else {
168
+ throw new Error("Failed to start proxy");
169
+ }
170
+ }
171
+
172
+ return {
173
+ port: server.port!,
174
+ stop: () => server.stop(),
175
+ };
176
+ }
177
+
178
+ /** streaming レスポンスを drain してバイト数をログに記録 */
179
+ async function drainAndLog(
180
+ stream: ReadableStream<Uint8Array>,
181
+ ctx: {
182
+ tracesDir: string;
183
+ method: string;
184
+ path: string;
185
+ status: number;
186
+ requestBytes: number;
187
+ startTime: number;
188
+ conductorId?: string;
189
+ taskId?: string;
190
+ role?: string;
191
+ }
192
+ ): Promise<void> {
193
+ let responseBytes = 0;
194
+ try {
195
+ const reader = stream.getReader();
196
+ while (true) {
197
+ const { done, value } = await reader.read();
198
+ if (done) break;
199
+ responseBytes += value.byteLength;
200
+ }
201
+ } catch {
202
+ // stream エラーは無視(クライアント切断等)
203
+ }
204
+
205
+ const entry: TraceEntry = {
206
+ timestamp: new Date().toISOString(),
207
+ conductor_id: ctx.conductorId,
208
+ task_id: ctx.taskId,
209
+ role: ctx.role,
210
+ method: ctx.method,
211
+ path: ctx.path,
212
+ status: ctx.status,
213
+ request_bytes: ctx.requestBytes,
214
+ response_bytes: responseBytes,
215
+ duration_ms: Date.now() - ctx.startTime,
216
+ };
217
+
218
+ appendFile(ctx.tracesDir, JSON.stringify(entry) + "\n").catch(() => {});
219
+ }
@@ -0,0 +1,73 @@
1
+ import { readdir, readFile, mkdir, rename, writeFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import { join, basename } from "path";
4
+ import { QueueMessage } from "./schema";
5
+
6
+ function getQueueDir(): string {
7
+ const root = process.env.PROJECT_ROOT || process.cwd();
8
+ return join(root, ".team/queue");
9
+ }
10
+
11
+ function getProcessedDir(): string {
12
+ return join(getQueueDir(), "processed");
13
+ }
14
+
15
+ export async function ensureQueueDirs(): Promise<void> {
16
+ await mkdir(getQueueDir(), { recursive: true });
17
+ await mkdir(getProcessedDir(), { recursive: true });
18
+ }
19
+
20
+ export async function readQueue(): Promise<
21
+ Array<{ path: string; message: QueueMessage }>
22
+ > {
23
+ const queueDir = getQueueDir();
24
+ if (!existsSync(queueDir)) return [];
25
+
26
+ const files = await readdir(queueDir);
27
+ const jsonFiles = files
28
+ .filter((f) => f.endsWith(".json"))
29
+ .sort();
30
+
31
+ const messages: Array<{ path: string; message: QueueMessage }> = [];
32
+
33
+ for (const file of jsonFiles) {
34
+ const filePath = join(queueDir, file);
35
+ try {
36
+ const raw = JSON.parse(await readFile(filePath, "utf-8"));
37
+ const message = QueueMessage.parse(raw);
38
+ messages.push({ path: filePath, message });
39
+ } catch (e) {
40
+ console.error(`[queue] invalid message: ${file}`, e);
41
+ await rename(filePath, join(getProcessedDir(), file)).catch(() => {});
42
+ }
43
+ }
44
+
45
+ return messages;
46
+ }
47
+
48
+ export async function markProcessed(filePath: string): Promise<void> {
49
+ const file = basename(filePath);
50
+ await rename(filePath, join(getProcessedDir(), file));
51
+ }
52
+
53
+ let sequence = 0;
54
+
55
+ export async function sendMessage(
56
+ message: QueueMessage
57
+ ): Promise<string> {
58
+ await ensureQueueDirs();
59
+
60
+ QueueMessage.parse(message);
61
+
62
+ sequence++;
63
+ const seq = String(sequence).padStart(3, "0");
64
+ const ts = Math.floor(Date.now() / 1000);
65
+ const fileName = `${seq}-${ts}-${message.type.toLowerCase()}.json`;
66
+ const filePath = join(getQueueDir(), fileName);
67
+ const tmpPath = `${filePath}.tmp`;
68
+
69
+ await writeFile(tmpPath, JSON.stringify(message, null, 2) + "\n");
70
+ await rename(tmpPath, filePath);
71
+
72
+ return filePath;
73
+ }
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+
3
+ // --- キューメッセージ ---
4
+
5
+ export const TaskCreatedMessage = z.object({
6
+ type: z.literal("TASK_CREATED"),
7
+ taskId: z.string(),
8
+ taskFile: z.string(),
9
+ timestamp: z.string().datetime(),
10
+ });
11
+
12
+ export const ConductorDoneMessage = z.object({
13
+ type: z.literal("CONDUCTOR_DONE"),
14
+ conductorId: z.string(),
15
+ sessionId: z.string().optional(),
16
+ transcriptPath: z.string().optional(),
17
+ surface: z.string(),
18
+ success: z.boolean(),
19
+ reason: z.string().optional(),
20
+ exitCode: z.number().optional(),
21
+ timestamp: z.string().datetime(),
22
+ });
23
+
24
+ export const AgentSpawnedMessage = z.object({
25
+ type: z.literal("AGENT_SPAWNED"),
26
+ conductorId: z.string(),
27
+ surface: z.string(),
28
+ role: z.string().optional(),
29
+ taskTitle: z.string().optional(),
30
+ timestamp: z.string().datetime(),
31
+ });
32
+
33
+ export const AgentDoneMessage = z.object({
34
+ type: z.literal("AGENT_DONE"),
35
+ conductorId: z.string(),
36
+ surface: z.string(),
37
+ timestamp: z.string().datetime(),
38
+ });
39
+
40
+ export const ShutdownMessage = z.object({
41
+ type: z.literal("SHUTDOWN"),
42
+ timestamp: z.string().datetime(),
43
+ });
44
+
45
+ export const QueueMessage = z.discriminatedUnion("type", [
46
+ TaskCreatedMessage,
47
+ ConductorDoneMessage,
48
+ AgentSpawnedMessage,
49
+ AgentDoneMessage,
50
+ ShutdownMessage,
51
+ ]);
52
+
53
+ export type QueueMessage = z.infer<typeof QueueMessage>;
54
+ export type TaskCreatedMessage = z.infer<typeof TaskCreatedMessage>;
55
+ export type ConductorDoneMessage = z.infer<typeof ConductorDoneMessage>;
56
+
57
+ // --- Agent 状態 ---
58
+
59
+ export interface AgentState {
60
+ surface: string;
61
+ role?: string;
62
+ taskTitle?: string;
63
+ spawnedAt: string;
64
+ }
65
+
66
+ // --- Conductor 状態 ---
67
+
68
+ export const ConductorState = z.object({
69
+ conductorId: z.string(),
70
+ taskRunId: z.string().optional(),
71
+ taskId: z.string().optional(),
72
+ taskTitle: z.string().optional(),
73
+ surface: z.string(),
74
+ worktreePath: z.string().optional(),
75
+ outputDir: z.string().optional(),
76
+ startedAt: z.string().datetime(),
77
+ });
78
+
79
+ export type ConductorState = z.infer<typeof ConductorState> & {
80
+ agents: AgentState[];
81
+ doneCandidate: boolean;
82
+ status: "idle" | "running" | "done";
83
+ paneId?: string;
84
+ };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * タスクファイルのパース・依存解決
3
+ */
4
+ import { readdir, readFile, writeFile } from "fs/promises";
5
+ import { existsSync } from "fs";
6
+ import { join } from "path";
7
+
8
+ export interface TaskMeta {
9
+ id: string;
10
+ title: string;
11
+ status: string;
12
+ priority: string;
13
+ dependsOn: string[];
14
+ filePath: string;
15
+ fileName: string;
16
+ createdAt: string; // ISO 8601 datetime
17
+ }
18
+
19
+ export interface TaskState {
20
+ status: string; // "draft" | "ready" | "in_progress" | "closed"
21
+ closedAt?: string; // ISO 8601
22
+ journal?: string; // 完了時のサマリー
23
+ }
24
+
25
+ export type TaskStateMap = Record<string, TaskState>;
26
+
27
+ /**
28
+ * YAML frontmatter からメタデータを抽出
29
+ */
30
+ export function parseTaskMeta(content: string, fileName: string, filePath: string): TaskMeta | null {
31
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
32
+ if (!fmMatch?.[1]) return null;
33
+
34
+ const fm = fmMatch[1];
35
+
36
+ const unquote = (s: string) => s.replace(/^["']|["']$/g, "");
37
+ const id = unquote(fm.match(/^id:\s*(.+)$/m)?.[1]?.trim() ?? "");
38
+ const title = unquote(fm.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? "");
39
+ const status = unquote(fm.match(/^status:\s*(.+)$/m)?.[1]?.trim() ?? "ready");
40
+ const priority = unquote(fm.match(/^priority:\s*(.+)$/m)?.[1]?.trim() ?? "medium");
41
+ const createdAt = unquote(fm.match(/^created_at:\s*(.+)$/m)?.[1]?.trim() ?? "");
42
+
43
+ // depends_on: [033, 034] or depends_on: 033
44
+ let dependsOn: string[] = [];
45
+ const depsMatch = fm.match(/^depends_on:\s*(.+)$/m);
46
+ if (depsMatch?.[1]) {
47
+ const raw = depsMatch[1].trim();
48
+ if (raw.startsWith("[")) {
49
+ // YAML array: [033, 034]
50
+ dependsOn = raw
51
+ .replace(/[\[\]]/g, "")
52
+ .split(",")
53
+ .map((s) => s.trim())
54
+ .filter(Boolean);
55
+ } else {
56
+ // single value: 033
57
+ dependsOn = [raw.trim()];
58
+ }
59
+ }
60
+
61
+ return {
62
+ id: id || fileName.match(/^(\d+)/)?.[1] || "",
63
+ title,
64
+ status,
65
+ priority,
66
+ dependsOn,
67
+ filePath,
68
+ fileName,
69
+ createdAt,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * task-state.json の読み込み
75
+ */
76
+ export async function loadTaskState(projectRoot: string): Promise<TaskStateMap> {
77
+ const filePath = join(projectRoot, ".team/task-state.json");
78
+ if (!existsSync(filePath)) return {};
79
+ try {
80
+ return JSON.parse(await readFile(filePath, "utf-8"));
81
+ } catch {
82
+ return {};
83
+ }
84
+ }
85
+
86
+ /**
87
+ * task-state.json の書き込み
88
+ */
89
+ export async function saveTaskState(projectRoot: string, state: TaskStateMap): Promise<void> {
90
+ const filePath = join(projectRoot, ".team/task-state.json");
91
+ await writeFile(filePath, JSON.stringify(state, null, 2) + "\n");
92
+ }
93
+
94
+ /**
95
+ * フラットな tasks/ からタスクを読み込み、task-state.json で状態を上書き
96
+ */
97
+ export async function loadTasks(projectRoot: string): Promise<{
98
+ tasks: TaskMeta[];
99
+ taskState: TaskStateMap;
100
+ }> {
101
+ const tasksDir = join(projectRoot, ".team/tasks");
102
+ const taskState = await loadTaskState(projectRoot);
103
+ const tasks: TaskMeta[] = [];
104
+
105
+ if (existsSync(tasksDir)) {
106
+ const files = await readdir(tasksDir);
107
+ for (const f of files) {
108
+ if (!f.endsWith(".md")) continue;
109
+ const filePath = join(tasksDir, f);
110
+ const content = await readFile(filePath, "utf-8");
111
+ const meta = parseTaskMeta(content, f, filePath);
112
+ if (meta) {
113
+ // task-state.json の状態で上書き(後方互換: なければファイルの frontmatter 値を使用)
114
+ if (taskState[meta.id]) {
115
+ meta.status = taskState[meta.id]!.status;
116
+ }
117
+ tasks.push(meta);
118
+ }
119
+ }
120
+ }
121
+
122
+ return { tasks, taskState };
123
+ }
124
+
125
+ /**
126
+ * 実行可能なタスクをフィルタリング
127
+ * - status: ready であること
128
+ * - depends_on の全タスクが closed に存在すること
129
+ */
130
+ export function filterExecutableTasks(
131
+ tasks: TaskMeta[],
132
+ closedIds: Set<string>,
133
+ assignedIds: Set<string>
134
+ ): TaskMeta[] {
135
+ return tasks.filter((task) => {
136
+ // status チェック
137
+ if (task.status !== "ready") return false;
138
+
139
+ // 既にアサイン済み
140
+ if (assignedIds.has(task.id)) return false;
141
+
142
+ // 依存チェック
143
+ if (task.dependsOn.length > 0) {
144
+ const allDepsResolved = task.dependsOn.every((dep) => closedIds.has(dep));
145
+ if (!allDepsResolved) return false;
146
+ }
147
+
148
+ return true;
149
+ });
150
+ }
151
+
152
+ /**
153
+ * 優先度ソート(high > medium > low)
154
+ */
155
+ export function sortByPriority(tasks: TaskMeta[]): TaskMeta[] {
156
+ const order: Record<string, number> = { high: 0, medium: 1, low: 2 };
157
+ return [...tasks].sort(
158
+ (a, b) => (order[a.priority] ?? 1) - (order[b.priority] ?? 1)
159
+ );
160
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * テンプレート検索・変数展開
3
+ */
4
+ import { existsSync } from "fs";
5
+ import { readFile, writeFile, mkdir, cp } from "fs/promises";
6
+ import { join, dirname } from "path";
7
+
8
+ export function findTemplateDir(): string | null {
9
+ // 1. daemon 自身からの相対パス(manager/ の兄弟 templates/)
10
+ // manager/template.ts → ../templates/
11
+ const fromSelf = join(dirname(import.meta.path), "../templates");
12
+ if (existsSync(join(fromSelf, "master.md"))) return fromSelf;
13
+
14
+ // 2. プロジェクトローカル
15
+ const projectRoot = process.env.PROJECT_ROOT || process.cwd();
16
+ const local = join(projectRoot, "skills/cmux-team/templates");
17
+ if (existsSync(join(local, "master.md"))) return local;
18
+
19
+ return null;
20
+ }
21
+
22
+ export async function generateMasterPrompt(
23
+ projectRoot: string
24
+ ): Promise<void> {
25
+ const promptsDir = join(projectRoot, ".team/prompts");
26
+ await mkdir(promptsDir, { recursive: true });
27
+ const dst = join(promptsDir, "master.md");
28
+
29
+ const templateDir = findTemplateDir();
30
+ if (!templateDir) {
31
+ throw new Error(
32
+ "Template directory not found. npm install -g cmux-team を実行してください"
33
+ );
34
+ }
35
+
36
+ await cp(join(templateDir, "master.md"), dst);
37
+ }
38
+
39
+ export async function generateConductorRolePrompt(
40
+ projectRoot: string
41
+ ): Promise<string> {
42
+ const templateDir = findTemplateDir();
43
+ if (!templateDir || !existsSync(join(templateDir, "conductor-role.md"))) {
44
+ throw new Error(
45
+ "Conductor role template not found. npm install -g cmux-team を実行してください"
46
+ );
47
+ }
48
+
49
+ const promptsDir = join(projectRoot, ".team/prompts");
50
+ await mkdir(promptsDir, { recursive: true });
51
+
52
+ const promptFile = join(promptsDir, "conductor-role.md");
53
+
54
+ let content = await readFile(join(templateDir, "conductor-role.md"), "utf-8");
55
+ content = content.replace(/\{\{PROJECT_ROOT\}\}/g, projectRoot);
56
+
57
+ await writeFile(promptFile, content);
58
+ return promptFile;
59
+ }
60
+
61
+ export async function generateConductorTaskPrompt(
62
+ projectRoot: string,
63
+ conductorId: string,
64
+ taskId: string,
65
+ taskContent: string,
66
+ worktreePath: string,
67
+ outputDir: string
68
+ ): Promise<string> {
69
+ const templateDir = findTemplateDir();
70
+ if (!templateDir || !existsSync(join(templateDir, "conductor-task.md"))) {
71
+ throw new Error(
72
+ "Conductor task template not found. npm install -g cmux-team を実行してください"
73
+ );
74
+ }
75
+
76
+ const promptsDir = join(projectRoot, ".team/prompts");
77
+ await mkdir(promptsDir, { recursive: true });
78
+
79
+ const promptFile = join(promptsDir, `${conductorId}.md`);
80
+
81
+ let content = await readFile(join(templateDir, "conductor-task.md"), "utf-8");
82
+
83
+ content = content
84
+ .replace(/\{\{TASK_CONTENT\}\}/g, taskContent)
85
+ .replace(/\{\{WORKTREE_PATH\}\}/g, worktreePath)
86
+ .replace(/\{\{OUTPUT_DIR\}\}/g, join(projectRoot, outputDir))
87
+ .replace(/\{\{PROJECT_ROOT\}\}/g, projectRoot)
88
+ .replace(/\{\{CONDUCTOR_ID\}\}/g, conductorId);
89
+
90
+ await writeFile(promptFile, content);
91
+ return promptFile;
92
+ }