@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.
- package/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +15 -0
- package/CHANGELOG.md +279 -0
- package/LICENSE +21 -0
- package/README.ja.md +238 -0
- package/README.md +158 -0
- package/bin/cmux-team.js +29 -0
- package/bin/postinstall.js +26 -0
- package/commands/master.md +8 -0
- package/commands/start.md +42 -0
- package/commands/team-archive.md +63 -0
- package/commands/team-design.md +199 -0
- package/commands/team-disband.md +79 -0
- package/commands/team-impl.md +201 -0
- package/commands/team-research.md +182 -0
- package/commands/team-review.md +222 -0
- package/commands/team-spec.md +133 -0
- package/commands/team-status.md +24 -0
- package/commands/team-sync-docs.md +127 -0
- package/commands/team-task.md +158 -0
- package/commands/team-test.md +230 -0
- package/package.json +51 -0
- package/skills/cmux-agent-role/SKILL.md +166 -0
- package/skills/cmux-team/SKILL.md +568 -0
- package/skills/cmux-team/manager/bun.lock +118 -0
- package/skills/cmux-team/manager/cmux.ts +134 -0
- package/skills/cmux-team/manager/conductor.ts +347 -0
- package/skills/cmux-team/manager/daemon.ts +373 -0
- package/skills/cmux-team/manager/e2e.ts +550 -0
- package/skills/cmux-team/manager/logger.ts +13 -0
- package/skills/cmux-team/manager/main.ts +756 -0
- package/skills/cmux-team/manager/master.ts +51 -0
- package/skills/cmux-team/manager/package.json +18 -0
- package/skills/cmux-team/manager/proxy.ts +219 -0
- package/skills/cmux-team/manager/queue.ts +73 -0
- package/skills/cmux-team/manager/schema.ts +84 -0
- package/skills/cmux-team/manager/task.ts +160 -0
- package/skills/cmux-team/manager/template.ts +92 -0
- package/skills/cmux-team/templates/architect.md +25 -0
- package/skills/cmux-team/templates/common-header.md +12 -0
- package/skills/cmux-team/templates/conductor-role.md +244 -0
- package/skills/cmux-team/templates/conductor-task.md +37 -0
- package/skills/cmux-team/templates/conductor.md +275 -0
- package/skills/cmux-team/templates/dockeeper.md +23 -0
- package/skills/cmux-team/templates/implementer.md +24 -0
- package/skills/cmux-team/templates/manager.md +199 -0
- package/skills/cmux-team/templates/master.md +94 -0
- package/skills/cmux-team/templates/researcher.md +24 -0
- package/skills/cmux-team/templates/reviewer.md +28 -0
- package/skills/cmux-team/templates/task-manager.md +22 -0
- package/skills/cmux-team/templates/tester.md +27 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* cmux-team — マルチエージェント開発オーケストレーション
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ./main.ts start # daemon 起動 + Master spawn + ダッシュボード
|
|
7
|
+
* ./main.ts send TASK_CREATED --task-id 035 --task-file ...
|
|
8
|
+
* ./main.ts send SHUTDOWN
|
|
9
|
+
* ./main.ts status # ダッシュボード表示
|
|
10
|
+
* ./main.ts status --log 20 # ログ末尾20行
|
|
11
|
+
* ./main.ts stop # graceful shutdown
|
|
12
|
+
* ./main.ts spawn-agent --conductor-id <id> --role <role> --prompt <prompt> [--pane <paneId>]
|
|
13
|
+
* ./main.ts agents # 稼働中エージェント一覧
|
|
14
|
+
* ./main.ts kill-agent --surface <s> [--conductor-id <id>]
|
|
15
|
+
* ./main.ts create-task --title <title> [--priority <p>] [--status <s>] [--body <text>]
|
|
16
|
+
* ./main.ts update-task --task-id <id> --status <status>
|
|
17
|
+
* ./main.ts close-task --task-id <id> [--journal <text>]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { join, dirname } from "path";
|
|
21
|
+
import { existsSync } from "fs";
|
|
22
|
+
import { readFile, readdir, writeFile, mkdir } from "fs/promises";
|
|
23
|
+
import { sendMessage, ensureQueueDirs } from "./queue";
|
|
24
|
+
import { createDaemon, initInfra, startMaster, initializeLayout, tick, updateTeamJson } from "./daemon";
|
|
25
|
+
import { startDashboard, unmountDashboard } from "./dashboard";
|
|
26
|
+
import { log } from "./logger";
|
|
27
|
+
import * as cmux from "./cmux";
|
|
28
|
+
import { start as startProxy } from "./proxy";
|
|
29
|
+
import { loadTaskState, saveTaskState } from "./task";
|
|
30
|
+
import type { QueueMessage } from "./schema";
|
|
31
|
+
|
|
32
|
+
// --- プロジェクトルート検出 ---
|
|
33
|
+
function findProjectRoot(): string {
|
|
34
|
+
// 環境変数
|
|
35
|
+
if (process.env.PROJECT_ROOT) return process.env.PROJECT_ROOT;
|
|
36
|
+
|
|
37
|
+
// .team/ を含むディレクトリを探す
|
|
38
|
+
let dir = process.cwd();
|
|
39
|
+
for (let i = 0; i < 10; i++) {
|
|
40
|
+
if (existsSync(join(dir, ".team"))) return dir;
|
|
41
|
+
const parent = join(dir, "..");
|
|
42
|
+
if (parent === dir) break;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return process.cwd();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 最新の main.ts を検索(npm グローバル → ローカル → 自分自身) */
|
|
50
|
+
function findLatestMainTs(): string {
|
|
51
|
+
const { execFileSync } = require("child_process");
|
|
52
|
+
|
|
53
|
+
// npm グローバルインストール先
|
|
54
|
+
try {
|
|
55
|
+
const npmGlobalPrefix = execFileSync("npm", ["prefix", "-g"]).toString().trim();
|
|
56
|
+
const npmMainTs = join(npmGlobalPrefix, "lib/node_modules/cmux-team/skills/cmux-team/manager/main.ts");
|
|
57
|
+
if (existsSync(npmMainTs)) return npmMainTs;
|
|
58
|
+
} catch {}
|
|
59
|
+
|
|
60
|
+
// ローカル
|
|
61
|
+
const local = join(process.cwd(), "skills/cmux-team/manager/main.ts");
|
|
62
|
+
if (existsSync(local)) return local;
|
|
63
|
+
|
|
64
|
+
// 自分自身
|
|
65
|
+
return process.argv[1] || import.meta.path;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const PROJECT_ROOT = findProjectRoot();
|
|
69
|
+
process.env.PROJECT_ROOT = PROJECT_ROOT;
|
|
70
|
+
process.chdir(PROJECT_ROOT);
|
|
71
|
+
|
|
72
|
+
// --- サブコマンド ---
|
|
73
|
+
const args = process.argv.slice(2);
|
|
74
|
+
const command = args[0];
|
|
75
|
+
|
|
76
|
+
function getArg(name: string): string | undefined {
|
|
77
|
+
const idx = args.indexOf(`--${name}`);
|
|
78
|
+
return idx >= 0 ? args[idx + 1] : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireArg(name: string): string {
|
|
82
|
+
const val = getArg(name);
|
|
83
|
+
if (!val) {
|
|
84
|
+
console.error(`Error: --${name} is required`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return val;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function cmdStart(): Promise<void> {
|
|
91
|
+
const state = await createDaemon(PROJECT_ROOT);
|
|
92
|
+
|
|
93
|
+
// team.json から Conductor 状態を復元(リロード時の二重起動防止)
|
|
94
|
+
try {
|
|
95
|
+
const teamJson = JSON.parse(await readFile(join(PROJECT_ROOT, ".team/team.json"), "utf-8"));
|
|
96
|
+
for (const c of teamJson.conductors ?? []) {
|
|
97
|
+
if (c.surface && await cmux.validateSurface(c.surface)) {
|
|
98
|
+
state.conductors.set(c.id, {
|
|
99
|
+
conductorId: c.id,
|
|
100
|
+
taskRunId: c.taskRunId,
|
|
101
|
+
taskId: c.taskId,
|
|
102
|
+
taskTitle: c.taskTitle,
|
|
103
|
+
surface: c.surface,
|
|
104
|
+
worktreePath: c.worktreePath,
|
|
105
|
+
outputDir: c.outputDir,
|
|
106
|
+
startedAt: c.startedAt ?? new Date().toISOString(),
|
|
107
|
+
agents: (c.agents ?? []).map((a: any) => ({
|
|
108
|
+
surface: a.surface,
|
|
109
|
+
role: a.role,
|
|
110
|
+
spawnedAt: a.spawnedAt ?? new Date().toISOString(),
|
|
111
|
+
})),
|
|
112
|
+
doneCandidate: false,
|
|
113
|
+
status: c.status || "running",
|
|
114
|
+
paneId: c.paneId,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (state.conductors.size > 0) {
|
|
119
|
+
await log("conductors_restored", `count=${state.conductors.size}`);
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
|
|
123
|
+
// インフラ準備
|
|
124
|
+
await initInfra(state);
|
|
125
|
+
await log(
|
|
126
|
+
"daemon_started",
|
|
127
|
+
`pid=${process.pid} poll=${state.pollInterval}ms max_conductors=${state.maxConductors}`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// ロギングプロキシ起動
|
|
131
|
+
let proxyHandle: { port: number; stop: () => void } | null = null;
|
|
132
|
+
try {
|
|
133
|
+
proxyHandle = await startProxy(PROJECT_ROOT, { getState: () => state });
|
|
134
|
+
await writeFile(join(PROJECT_ROOT, ".team/proxy-port"), String(proxyHandle.port));
|
|
135
|
+
await log("proxy_started", `port=${proxyHandle.port}`);
|
|
136
|
+
} catch (e: any) {
|
|
137
|
+
await log("proxy_start_failed", e.message);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// daemon surface 取得
|
|
141
|
+
let daemonSurface: string | undefined;
|
|
142
|
+
try {
|
|
143
|
+
daemonSurface = await cmux.getCallerSurface();
|
|
144
|
+
await log("daemon_surface", `surface=${daemonSurface}`);
|
|
145
|
+
} catch (e: any) {
|
|
146
|
+
await log("daemon_surface_fallback", e.message);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Conductor を先に作成(全インフラ準備完了後に Master を起動)
|
|
150
|
+
await initializeLayout(state, daemonSurface);
|
|
151
|
+
|
|
152
|
+
// Master spawn(最後に作成)
|
|
153
|
+
await startMaster(state, daemonSurface);
|
|
154
|
+
|
|
155
|
+
await updateTeamJson(state);
|
|
156
|
+
|
|
157
|
+
// シグナルハンドリング
|
|
158
|
+
const shutdown = async () => {
|
|
159
|
+
state.running = false;
|
|
160
|
+
proxyHandle?.stop();
|
|
161
|
+
await log("daemon_stopped");
|
|
162
|
+
await updateTeamJson(state);
|
|
163
|
+
process.exit(0);
|
|
164
|
+
};
|
|
165
|
+
process.on("SIGINT", shutdown);
|
|
166
|
+
process.on("SIGTERM", shutdown);
|
|
167
|
+
|
|
168
|
+
// バージョン取得(plugin.json から)
|
|
169
|
+
let version: string | undefined;
|
|
170
|
+
try {
|
|
171
|
+
const pluginJsonPath = join(dirname(import.meta.path), "../../..", ".claude-plugin/plugin.json");
|
|
172
|
+
if (existsSync(pluginJsonPath)) {
|
|
173
|
+
version = JSON.parse(await readFile(pluginJsonPath, "utf-8")).version;
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
|
|
177
|
+
// ダッシュボード表示(キーボードショートカット付き)
|
|
178
|
+
startDashboard(() => state, {
|
|
179
|
+
version,
|
|
180
|
+
onReload: async () => {
|
|
181
|
+
// ink を解放し、exec でプロセスを置換(PID は変わらない、env は完全に引き継ぐ)
|
|
182
|
+
unmountDashboard();
|
|
183
|
+
const latestMainTs = findLatestMainTs();
|
|
184
|
+
await log("daemon_reload");
|
|
185
|
+
await log("daemon_reload_target", latestMainTs);
|
|
186
|
+
state.running = false;
|
|
187
|
+
// execSync で自プロセスを置換(bun → bash exec → bun)
|
|
188
|
+
const { execFileSync } = require("child_process");
|
|
189
|
+
try {
|
|
190
|
+
execFileSync("bash", ["-c", `exec bun run "${latestMainTs}" start`], {
|
|
191
|
+
stdio: "inherit",
|
|
192
|
+
env: process.env,
|
|
193
|
+
cwd: process.cwd(),
|
|
194
|
+
});
|
|
195
|
+
} catch {}
|
|
196
|
+
process.exit(0);
|
|
197
|
+
},
|
|
198
|
+
onQuit: () => { shutdown(); },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// メインループ
|
|
202
|
+
while (state.running) {
|
|
203
|
+
try {
|
|
204
|
+
await tick(state);
|
|
205
|
+
await updateTeamJson(state);
|
|
206
|
+
} catch (e: any) {
|
|
207
|
+
await log("error", `tick: ${e.message}`);
|
|
208
|
+
}
|
|
209
|
+
await sleep(state.pollInterval);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await shutdown();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function cmdSend(): Promise<void> {
|
|
216
|
+
await ensureQueueDirs();
|
|
217
|
+
const type = args[1];
|
|
218
|
+
const now = new Date().toISOString();
|
|
219
|
+
|
|
220
|
+
let message: QueueMessage;
|
|
221
|
+
|
|
222
|
+
switch (type) {
|
|
223
|
+
case "TASK_CREATED":
|
|
224
|
+
message = {
|
|
225
|
+
type: "TASK_CREATED",
|
|
226
|
+
taskId: requireArg("task-id"),
|
|
227
|
+
taskFile: requireArg("task-file"),
|
|
228
|
+
timestamp: now,
|
|
229
|
+
};
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case "CONDUCTOR_DONE":
|
|
233
|
+
message = {
|
|
234
|
+
type: "CONDUCTOR_DONE",
|
|
235
|
+
conductorId: requireArg("conductor-id"),
|
|
236
|
+
surface: getArg("surface") || "unknown",
|
|
237
|
+
success: getArg("success") !== "false", // デフォルト true(後方互換)
|
|
238
|
+
reason: getArg("reason"),
|
|
239
|
+
exitCode: getArg("exit-code") ? Number(getArg("exit-code")) : undefined,
|
|
240
|
+
sessionId: getArg("session-id"),
|
|
241
|
+
transcriptPath: getArg("transcript-path"),
|
|
242
|
+
timestamp: now,
|
|
243
|
+
};
|
|
244
|
+
break;
|
|
245
|
+
|
|
246
|
+
case "AGENT_SPAWNED":
|
|
247
|
+
message = {
|
|
248
|
+
type: "AGENT_SPAWNED",
|
|
249
|
+
conductorId: requireArg("conductor-id"),
|
|
250
|
+
surface: requireArg("surface"),
|
|
251
|
+
role: getArg("role"),
|
|
252
|
+
taskTitle: getArg("task-title"),
|
|
253
|
+
timestamp: now,
|
|
254
|
+
};
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case "AGENT_DONE":
|
|
258
|
+
message = {
|
|
259
|
+
type: "AGENT_DONE",
|
|
260
|
+
conductorId: requireArg("conductor-id"),
|
|
261
|
+
surface: requireArg("surface"),
|
|
262
|
+
timestamp: now,
|
|
263
|
+
};
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case "SHUTDOWN":
|
|
267
|
+
message = { type: "SHUTDOWN", timestamp: now };
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
default:
|
|
271
|
+
console.error("Usage: send <TASK_CREATED|CONDUCTOR_DONE|AGENT_SPAWNED|AGENT_DONE|SHUTDOWN>");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const path = await sendMessage(message);
|
|
276
|
+
console.log(`OK ${path}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function cmdStatus(): Promise<void> {
|
|
280
|
+
const teamJsonPath = join(PROJECT_ROOT, ".team/team.json");
|
|
281
|
+
if (!existsSync(teamJsonPath)) {
|
|
282
|
+
console.log("チーム未起動。`start` で起動してください。");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const teamJson = JSON.parse(await readFile(teamJsonPath, "utf-8"));
|
|
287
|
+
const pid = teamJson.manager?.pid;
|
|
288
|
+
const alive = pid && isProcessAlive(pid);
|
|
289
|
+
const masterSurface = teamJson.master?.surface;
|
|
290
|
+
const conductors: Array<{ id: string; taskId: string; taskTitle?: string; surface: string }> = teamJson.conductors || [];
|
|
291
|
+
const logLines = getArg("log") || "10";
|
|
292
|
+
|
|
293
|
+
// --- ヘッダー ---
|
|
294
|
+
const status = alive ? "RUNNING" : "STOPPED";
|
|
295
|
+
console.log(`cmux-team ${status} PID ${pid || "-"} conductors ${conductors.length}`);
|
|
296
|
+
|
|
297
|
+
// --- Master ---
|
|
298
|
+
console.log(`─ Master ${"─".repeat(50)}`);
|
|
299
|
+
if (masterSurface) {
|
|
300
|
+
console.log(` ● [${masterSurface.replace("surface:", "")}]`);
|
|
301
|
+
} else {
|
|
302
|
+
console.log(` ○ not spawned`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// --- Conductors ---
|
|
306
|
+
console.log(`─ Conductors ${conductors.length} ${"─".repeat(44)}`);
|
|
307
|
+
if (conductors.length === 0) {
|
|
308
|
+
console.log(` idle`);
|
|
309
|
+
} else {
|
|
310
|
+
for (const c of conductors) {
|
|
311
|
+
const title = c.taskTitle ? ` ${c.taskTitle}` : "";
|
|
312
|
+
console.log(` ● [${c.surface.replace("surface:", "")}] #${c.taskId}${title} ${c.id}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- Tasks ---
|
|
317
|
+
const taskState = await loadTaskState(PROJECT_ROOT);
|
|
318
|
+
const tasksDir = join(PROJECT_ROOT, ".team/tasks");
|
|
319
|
+
let totalCount = 0;
|
|
320
|
+
try { totalCount = (await readdir(tasksDir)).filter(f => f.endsWith(".md")).length; } catch {}
|
|
321
|
+
const closedCount = Object.values(taskState).filter(s => s.status === "closed").length;
|
|
322
|
+
const openCount = totalCount - closedCount;
|
|
323
|
+
console.log(`─ Tasks ${"─".repeat(51)}`);
|
|
324
|
+
console.log(` open: ${openCount} closed: ${closedCount}`);
|
|
325
|
+
|
|
326
|
+
// --- Log tail ---
|
|
327
|
+
const n = Math.max(1, parseInt(logLines, 10) || 10);
|
|
328
|
+
console.log(`─ Log (last ${n}) ${"─".repeat(Math.max(0, 42 - String(n).length))}`);
|
|
329
|
+
try {
|
|
330
|
+
const log = await readFile(join(PROJECT_ROOT, ".team/logs/manager.log"), "utf-8");
|
|
331
|
+
const lines = log.trim().split("\n").filter(Boolean).slice(-n);
|
|
332
|
+
for (const line of lines) {
|
|
333
|
+
const m = line.match(/^\[([^\]]+)\]\s+(.*)/);
|
|
334
|
+
if (m) {
|
|
335
|
+
const utcTs = m[1] ?? "";
|
|
336
|
+
const time = new Date(utcTs).toLocaleTimeString("ja-JP", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
|
|
337
|
+
console.log(` ${time} ${m[2]}`);
|
|
338
|
+
} else {
|
|
339
|
+
console.log(` ${line}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
console.log(` (no log)`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function cmdStop(): Promise<void> {
|
|
348
|
+
await ensureQueueDirs();
|
|
349
|
+
const path = await sendMessage({
|
|
350
|
+
type: "SHUTDOWN",
|
|
351
|
+
timestamp: new Date().toISOString(),
|
|
352
|
+
});
|
|
353
|
+
console.log(`SHUTDOWN sent: ${path}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function cmdSpawnAgent(): Promise<void> {
|
|
357
|
+
const conductorId = requireArg("conductor-id");
|
|
358
|
+
const role = requireArg("role");
|
|
359
|
+
const prompt = getArg("prompt");
|
|
360
|
+
const promptFile = getArg("prompt-file");
|
|
361
|
+
const taskTitle = getArg("task-title");
|
|
362
|
+
const pane = getArg("pane");
|
|
363
|
+
|
|
364
|
+
if (!prompt && !promptFile) {
|
|
365
|
+
console.error("Error: --prompt or --prompt-file is required");
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- 1. プロキシポート読み取り + 生存確認 ---
|
|
370
|
+
const proxyPortFile = join(PROJECT_ROOT, ".team/proxy-port");
|
|
371
|
+
let proxyPort: string | undefined;
|
|
372
|
+
try {
|
|
373
|
+
const port = (await readFile(proxyPortFile, "utf-8")).trim();
|
|
374
|
+
// 実際に接続できるか確認してから採用
|
|
375
|
+
const alive = await new Promise<boolean>((resolve) => {
|
|
376
|
+
const net = require("net");
|
|
377
|
+
const sock = net.connect({ port: Number(port), host: "127.0.0.1", timeout: 1000 }, () => {
|
|
378
|
+
sock.destroy();
|
|
379
|
+
resolve(true);
|
|
380
|
+
});
|
|
381
|
+
sock.on("error", () => resolve(false));
|
|
382
|
+
sock.on("timeout", () => { sock.destroy(); resolve(false); });
|
|
383
|
+
});
|
|
384
|
+
if (alive) {
|
|
385
|
+
proxyPort = port;
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// プロキシ未起動の場合はなしで続行
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- 2. タブ作成(--pane 直接指定 → team.json lookup → split フォールバック) ---
|
|
392
|
+
let paneId: string | undefined = pane; // --pane が最優先
|
|
393
|
+
let worktreePath: string | undefined;
|
|
394
|
+
try {
|
|
395
|
+
const teamJson = JSON.parse(await readFile(join(PROJECT_ROOT, ".team/team.json"), "utf-8"));
|
|
396
|
+
const conductor = teamJson.conductors?.find((c: any) => c.id === conductorId);
|
|
397
|
+
if (!paneId) paneId = conductor?.paneId;
|
|
398
|
+
worktreePath = conductor?.worktreePath;
|
|
399
|
+
} catch {}
|
|
400
|
+
|
|
401
|
+
let surface: string;
|
|
402
|
+
if (paneId) {
|
|
403
|
+
surface = await cmux.newSurface(paneId);
|
|
404
|
+
} else {
|
|
405
|
+
surface = await cmux.newSplit("down");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!(await cmux.validateSurface(surface))) {
|
|
409
|
+
console.error(`Error: surface ${surface} validation failed`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- 3. Claude Code 起動 ---
|
|
414
|
+
// 環境変数を export(Conductor のシェルセッションに永続化し子プロセスに自動継承)
|
|
415
|
+
const exports: string[] = [
|
|
416
|
+
`export CONDUCTOR_ID=${conductorId}`,
|
|
417
|
+
`export ROLE=${role}`,
|
|
418
|
+
`export PROJECT_ROOT=${PROJECT_ROOT}`,
|
|
419
|
+
];
|
|
420
|
+
if (proxyPort) {
|
|
421
|
+
exports.push(`export ANTHROPIC_BASE_URL=http://127.0.0.1:${proxyPort}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const cdPrefix = worktreePath ? `cd ${worktreePath} && ` : "";
|
|
425
|
+
|
|
426
|
+
let claudeCmd: string;
|
|
427
|
+
if (promptFile) {
|
|
428
|
+
// --bare は OAuth 認証(Claude Max)をスキップするため使用しない
|
|
429
|
+
claudeCmd = `${cdPrefix}${exports.join(" && ")} && claude --dangerously-skip-permissions '${promptFile} を読んで指示に従ってください。'`;
|
|
430
|
+
} else {
|
|
431
|
+
// 後方互換: --prompt でインライン渡し
|
|
432
|
+
claudeCmd = `${cdPrefix}${exports.join(" && ")} && claude --dangerously-skip-permissions '${prompt}'`;
|
|
433
|
+
}
|
|
434
|
+
await cmux.send(surface, claudeCmd + "\n");
|
|
435
|
+
|
|
436
|
+
// --- 4. Trust 承認 ---
|
|
437
|
+
await cmux.waitForTrust(surface);
|
|
438
|
+
|
|
439
|
+
// --- 5. タブ名設定 ---
|
|
440
|
+
const roleIcons: Record<string, string> = {
|
|
441
|
+
researcher: "🔍", research: "🔍",
|
|
442
|
+
architect: "📐", design: "📐",
|
|
443
|
+
implementer: "⚙", impl: "⚙",
|
|
444
|
+
reviewer: "👀", review: "👀",
|
|
445
|
+
tester: "🧪", test: "🧪",
|
|
446
|
+
dockeeper: "📝", docs: "📝",
|
|
447
|
+
"task-manager": "📋",
|
|
448
|
+
};
|
|
449
|
+
const roleIcon = roleIcons[role] ?? "▸";
|
|
450
|
+
const num = surface.replace("surface:", "");
|
|
451
|
+
const shortTitle = taskTitle
|
|
452
|
+
? (taskTitle.length > 25 ? taskTitle.slice(0, 25) + "…" : taskTitle)
|
|
453
|
+
: "";
|
|
454
|
+
const tabName = shortTitle ? `[${num}] ${roleIcon} ${shortTitle}` : `[${num}] ${roleIcon} ${role}`;
|
|
455
|
+
await cmux.renameTab(surface, tabName);
|
|
456
|
+
|
|
457
|
+
// --- 6. AGENT_SPAWNED をキューに送信 ---
|
|
458
|
+
await ensureQueueDirs();
|
|
459
|
+
await sendMessage({
|
|
460
|
+
type: "AGENT_SPAWNED",
|
|
461
|
+
conductorId,
|
|
462
|
+
surface,
|
|
463
|
+
role,
|
|
464
|
+
taskTitle,
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// --- 7. stdout に surface を出力 ---
|
|
469
|
+
console.log(`SURFACE=${surface}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function cmdAgents(): Promise<void> {
|
|
473
|
+
const teamJsonPath = join(PROJECT_ROOT, ".team/team.json");
|
|
474
|
+
if (!existsSync(teamJsonPath)) {
|
|
475
|
+
console.log("チーム未起動。");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const teamJson = JSON.parse(await readFile(teamJsonPath, "utf-8"));
|
|
480
|
+
const conductors: Array<{
|
|
481
|
+
id: string;
|
|
482
|
+
taskId: string;
|
|
483
|
+
taskTitle?: string;
|
|
484
|
+
surface: string;
|
|
485
|
+
agents?: Array<{ surface: string; role?: string }>;
|
|
486
|
+
}> = teamJson.conductors || [];
|
|
487
|
+
|
|
488
|
+
let agentCount = 0;
|
|
489
|
+
for (const c of conductors) {
|
|
490
|
+
const agents = c.agents || [];
|
|
491
|
+
for (const a of agents) {
|
|
492
|
+
agentCount++;
|
|
493
|
+
const rolePart = a.role ? `role=${a.role}` : "role=unknown";
|
|
494
|
+
console.log(`${a.surface} ${rolePart} conductor=${c.id} task=${c.taskId}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (agentCount === 0) {
|
|
499
|
+
console.log("稼働中のエージェントはありません。");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function cmdKillAgent(): Promise<void> {
|
|
504
|
+
const surface = requireArg("surface");
|
|
505
|
+
const conductorId = getArg("conductor-id");
|
|
506
|
+
|
|
507
|
+
// --- 1. surface を閉じる ---
|
|
508
|
+
await cmux.closeSurface(surface);
|
|
509
|
+
|
|
510
|
+
// --- 2. AGENT_DONE をキューに送信 ---
|
|
511
|
+
if (conductorId) {
|
|
512
|
+
await ensureQueueDirs();
|
|
513
|
+
await sendMessage({
|
|
514
|
+
type: "AGENT_DONE",
|
|
515
|
+
conductorId,
|
|
516
|
+
surface,
|
|
517
|
+
timestamp: new Date().toISOString(),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
console.log(`OK killed ${surface}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function cmdCreateTask(): Promise<void> {
|
|
525
|
+
const title = requireArg("title");
|
|
526
|
+
const priority = getArg("priority") || "medium";
|
|
527
|
+
const status = getArg("status") || "draft";
|
|
528
|
+
const body = getArg("body") || "";
|
|
529
|
+
|
|
530
|
+
// slug 生成
|
|
531
|
+
let slug = title
|
|
532
|
+
.toLowerCase()
|
|
533
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
534
|
+
.replace(/-{2,}/g, "-")
|
|
535
|
+
.replace(/^-|-$/g, "");
|
|
536
|
+
if (!slug) slug = "task";
|
|
537
|
+
|
|
538
|
+
// 最大 ID 取得
|
|
539
|
+
const tasksDir = join(PROJECT_ROOT, ".team/tasks");
|
|
540
|
+
await mkdir(tasksDir, { recursive: true });
|
|
541
|
+
|
|
542
|
+
let maxId = 0;
|
|
543
|
+
try {
|
|
544
|
+
const files = await readdir(tasksDir);
|
|
545
|
+
for (const f of files) {
|
|
546
|
+
const n = parseInt(f, 10);
|
|
547
|
+
if (!isNaN(n) && n > maxId) maxId = n;
|
|
548
|
+
}
|
|
549
|
+
} catch {}
|
|
550
|
+
|
|
551
|
+
const newId = String(maxId + 1).padStart(3, "0");
|
|
552
|
+
const fileName = `${newId}-${slug}.md`;
|
|
553
|
+
const filePath = join(tasksDir, fileName);
|
|
554
|
+
|
|
555
|
+
// タスクファイル生成(status は含めない — task-state.json で管理)
|
|
556
|
+
const content = `---
|
|
557
|
+
id: ${newId}
|
|
558
|
+
title: ${title}
|
|
559
|
+
priority: ${priority}
|
|
560
|
+
created_at: ${new Date().toISOString()}
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## タスク
|
|
564
|
+
${body}
|
|
565
|
+
`;
|
|
566
|
+
await writeFile(filePath, content);
|
|
567
|
+
|
|
568
|
+
// task-state.json に初期状態を書き込む
|
|
569
|
+
const taskState = await loadTaskState(PROJECT_ROOT);
|
|
570
|
+
taskState[newId] = { status };
|
|
571
|
+
await saveTaskState(PROJECT_ROOT, taskState);
|
|
572
|
+
|
|
573
|
+
// status が ready の場合のみ TASK_CREATED を送信
|
|
574
|
+
if (status === "ready") {
|
|
575
|
+
await ensureQueueDirs();
|
|
576
|
+
await sendMessage({
|
|
577
|
+
type: "TASK_CREATED",
|
|
578
|
+
taskId: newId,
|
|
579
|
+
taskFile: filePath,
|
|
580
|
+
timestamp: new Date().toISOString(),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const relPath = `.team/tasks/${fileName}`;
|
|
585
|
+
console.log(`TASK_ID=${newId} FILE=${relPath}`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function cmdUpdateTask(): Promise<void> {
|
|
589
|
+
const taskId = requireArg("task-id");
|
|
590
|
+
const newStatus = requireArg("status");
|
|
591
|
+
|
|
592
|
+
// tasks/ からタスクファイルを検索(存在確認のみ)
|
|
593
|
+
const tasksDir = join(PROJECT_ROOT, ".team/tasks");
|
|
594
|
+
let taskFile: string | undefined;
|
|
595
|
+
try {
|
|
596
|
+
const files = await readdir(tasksDir);
|
|
597
|
+
for (const f of files) {
|
|
598
|
+
if (f.endsWith(".md") && f.startsWith(taskId)) {
|
|
599
|
+
taskFile = join(tasksDir, f);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch {}
|
|
604
|
+
|
|
605
|
+
if (!taskFile) {
|
|
606
|
+
// ファイル名が数値IDで始まらない場合、frontmatter の id でも検索
|
|
607
|
+
try {
|
|
608
|
+
const files = await readdir(tasksDir);
|
|
609
|
+
for (const f of files) {
|
|
610
|
+
if (!f.endsWith(".md")) continue;
|
|
611
|
+
const content = await readFile(join(tasksDir, f), "utf-8");
|
|
612
|
+
const idMatch = content.match(/^id:\s*(.+)$/m);
|
|
613
|
+
if (idMatch && idMatch[1]?.trim() === taskId) {
|
|
614
|
+
taskFile = join(tasksDir, f);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch {}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!taskFile) {
|
|
622
|
+
console.error(`Error: task ${taskId} not found in .team/tasks/`);
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// task-state.json の status を更新(ファイル自体は変更しない)
|
|
627
|
+
const taskState = await loadTaskState(PROJECT_ROOT);
|
|
628
|
+
taskState[taskId] = { ...taskState[taskId], status: newStatus };
|
|
629
|
+
await saveTaskState(PROJECT_ROOT, taskState);
|
|
630
|
+
|
|
631
|
+
// ready に変更された場合は TASK_CREATED を送信
|
|
632
|
+
if (newStatus === "ready") {
|
|
633
|
+
await ensureQueueDirs();
|
|
634
|
+
await sendMessage({
|
|
635
|
+
type: "TASK_CREATED",
|
|
636
|
+
taskId,
|
|
637
|
+
taskFile,
|
|
638
|
+
timestamp: new Date().toISOString(),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
console.log(`OK updated ${taskId} status=${newStatus}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function cmdCloseTask(): Promise<void> {
|
|
646
|
+
const taskId = requireArg("task-id");
|
|
647
|
+
const journal = getArg("journal");
|
|
648
|
+
|
|
649
|
+
// tasks/ からタスクファイルを検索(存在確認のみ)
|
|
650
|
+
const tasksDir = join(PROJECT_ROOT, ".team/tasks");
|
|
651
|
+
let taskFile: string | undefined;
|
|
652
|
+
try {
|
|
653
|
+
const files = await readdir(tasksDir);
|
|
654
|
+
for (const f of files) {
|
|
655
|
+
if (f.endsWith(".md") && f.startsWith(taskId)) {
|
|
656
|
+
taskFile = join(tasksDir, f);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} catch {}
|
|
661
|
+
|
|
662
|
+
if (!taskFile) {
|
|
663
|
+
// frontmatter の id でも検索
|
|
664
|
+
try {
|
|
665
|
+
const files = await readdir(tasksDir);
|
|
666
|
+
for (const f of files) {
|
|
667
|
+
if (!f.endsWith(".md")) continue;
|
|
668
|
+
const content = await readFile(join(tasksDir, f), "utf-8");
|
|
669
|
+
const idMatch = content.match(/^id:\s*(.+)$/m);
|
|
670
|
+
if (idMatch && idMatch[1]?.trim() === taskId) {
|
|
671
|
+
taskFile = join(tasksDir, f);
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch {}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!taskFile) {
|
|
679
|
+
console.error(`Error: task ${taskId} not found in .team/tasks/`);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// task-state.json で closed + closedAt + journal を設定(ファイルは移動しない)
|
|
684
|
+
const taskState = await loadTaskState(PROJECT_ROOT);
|
|
685
|
+
taskState[taskId] = {
|
|
686
|
+
status: "closed",
|
|
687
|
+
closedAt: new Date().toISOString(),
|
|
688
|
+
...(journal ? { journal } : {}),
|
|
689
|
+
};
|
|
690
|
+
await saveTaskState(PROJECT_ROOT, taskState);
|
|
691
|
+
|
|
692
|
+
console.log(`OK closed ${taskId}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function isProcessAlive(pid: number): boolean {
|
|
696
|
+
try {
|
|
697
|
+
process.kill(pid, 0);
|
|
698
|
+
return true;
|
|
699
|
+
} catch {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function sleep(ms: number): Promise<void> {
|
|
705
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// --- ルーティング ---
|
|
709
|
+
switch (command) {
|
|
710
|
+
case "start":
|
|
711
|
+
await cmdStart();
|
|
712
|
+
break;
|
|
713
|
+
case "send":
|
|
714
|
+
await cmdSend();
|
|
715
|
+
break;
|
|
716
|
+
case "status":
|
|
717
|
+
await cmdStatus();
|
|
718
|
+
break;
|
|
719
|
+
case "stop":
|
|
720
|
+
await cmdStop();
|
|
721
|
+
break;
|
|
722
|
+
case "spawn-agent":
|
|
723
|
+
await cmdSpawnAgent();
|
|
724
|
+
break;
|
|
725
|
+
case "agents":
|
|
726
|
+
await cmdAgents();
|
|
727
|
+
break;
|
|
728
|
+
case "kill-agent":
|
|
729
|
+
await cmdKillAgent();
|
|
730
|
+
break;
|
|
731
|
+
case "create-task":
|
|
732
|
+
await cmdCreateTask();
|
|
733
|
+
break;
|
|
734
|
+
case "update-task":
|
|
735
|
+
await cmdUpdateTask();
|
|
736
|
+
break;
|
|
737
|
+
case "close-task":
|
|
738
|
+
await cmdCloseTask();
|
|
739
|
+
break;
|
|
740
|
+
default:
|
|
741
|
+
console.log(`cmux-team — マルチエージェント開発オーケストレーション
|
|
742
|
+
|
|
743
|
+
Usage:
|
|
744
|
+
cmux-team start daemon 起動 + Master spawn
|
|
745
|
+
cmux-team send TASK_CREATED --task-id <id> --task-file <path>
|
|
746
|
+
cmux-team send SHUTDOWN
|
|
747
|
+
cmux-team status ステータス表示
|
|
748
|
+
cmux-team stop graceful shutdown
|
|
749
|
+
cmux-team spawn-agent --conductor-id <id> --role <role> --prompt <prompt> [--pane <paneId>]
|
|
750
|
+
cmux-team agents 稼働中エージェント一覧
|
|
751
|
+
cmux-team kill-agent --surface <surface> [--conductor-id <id>]
|
|
752
|
+
cmux-team create-task --title <title> [--priority <p>] [--status <s>] [--body <text>]
|
|
753
|
+
cmux-team update-task --task-id <id> --status <status>
|
|
754
|
+
cmux-team close-task --task-id <id> [--journal <text>]`);
|
|
755
|
+
break;
|
|
756
|
+
}
|