@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,373 @@
1
+ /**
2
+ * Daemon — メインループ + surface 管理
3
+ */
4
+ import { readdir, readFile, writeFile, mkdir } from "fs/promises";
5
+ import { existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { readQueue, markProcessed, ensureQueueDirs } from "./queue";
8
+ import {
9
+ spawnConductor,
10
+ checkConductorStatus,
11
+ collectResults,
12
+ initializeConductorSlots,
13
+ assignTask,
14
+ resetConductor,
15
+ } from "./conductor";
16
+ import { spawnMaster, isMasterAlive } from "./master";
17
+ import * as cmux from "./cmux";
18
+ import { loadTasks, filterExecutableTasks, sortByPriority } from "./task";
19
+ import { log } from "./logger";
20
+ import type { ConductorState } from "./schema";
21
+
22
+ export interface TaskSummary {
23
+ id: string;
24
+ title: string;
25
+ status: string;
26
+ createdAt: string;
27
+ closedAt?: string;
28
+ }
29
+
30
+ export interface DaemonState {
31
+ running: boolean;
32
+ masterSurface: string | null;
33
+ conductors: Map<string, ConductorState>;
34
+ projectRoot: string;
35
+ pollInterval: number;
36
+ maxConductors: number;
37
+ lastUpdate: Date;
38
+ pendingTasks: number;
39
+ openTasks: number;
40
+ taskList: TaskSummary[];
41
+ }
42
+
43
+ /** conductorId または taskRunId で Conductor を検索 */
44
+ function findConductor(state: DaemonState, id: string): ConductorState | undefined {
45
+ const direct = state.conductors.get(id);
46
+ if (direct) return direct;
47
+ // taskRunId で検索(Conductor セッションが taskRunId を conductorId として送信する場合)
48
+ for (const c of state.conductors.values()) {
49
+ if (c.taskRunId === id) return c;
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ export async function createDaemon(projectRoot: string): Promise<DaemonState> {
55
+ return {
56
+ running: true,
57
+ masterSurface: null,
58
+ conductors: new Map(),
59
+ projectRoot,
60
+ pollInterval: Number(process.env.CMUX_TEAM_POLL_INTERVAL ?? 10_000),
61
+ maxConductors: Number(process.env.CMUX_TEAM_MAX_CONDUCTORS ?? 3),
62
+ lastUpdate: new Date(),
63
+ pendingTasks: 0,
64
+ openTasks: 0,
65
+ taskList: [],
66
+ };
67
+ }
68
+
69
+ export async function initInfra(state: DaemonState): Promise<void> {
70
+ const root = state.projectRoot;
71
+ await mkdir(join(root, ".team/tasks"), { recursive: true });
72
+ await mkdir(join(root, ".team/output"), { recursive: true });
73
+ await mkdir(join(root, ".team/prompts"), { recursive: true });
74
+ await mkdir(join(root, ".team/logs"), { recursive: true });
75
+ await ensureQueueDirs();
76
+
77
+ const scriptsDir = join(root, ".team/scripts");
78
+ await mkdir(scriptsDir, { recursive: true });
79
+
80
+ // .gitignore
81
+ const gitignore = join(root, ".team/.gitignore");
82
+ if (!existsSync(gitignore)) {
83
+ await writeFile(
84
+ gitignore,
85
+ "output/\nprompts/\ndocs-snapshot/\nlogs/\nqueue/\ntask-state.json\n"
86
+ );
87
+ }
88
+
89
+ // team.json
90
+ const teamJson = join(root, ".team/team.json");
91
+ if (!existsSync(teamJson)) {
92
+ await writeFile(
93
+ teamJson,
94
+ JSON.stringify(
95
+ {
96
+ project: "",
97
+ phase: "init",
98
+ architecture: "4-tier",
99
+ master: {},
100
+ manager: {},
101
+ conductors: [],
102
+ },
103
+ null,
104
+ 2
105
+ ) + "\n"
106
+ );
107
+ }
108
+ }
109
+
110
+ export async function startMaster(state: DaemonState, daemonSurface?: string): Promise<void> {
111
+ // 既存 Master の存在チェック
112
+ try {
113
+ const teamJson = JSON.parse(
114
+ await readFile(join(state.projectRoot, ".team/team.json"), "utf-8")
115
+ );
116
+ const surface = teamJson.master?.surface;
117
+ if (surface) {
118
+ const alive = await isMasterAlive(surface);
119
+ if (alive) {
120
+ state.masterSurface = surface;
121
+ await log("master_alive", `surface=${surface}`);
122
+ return;
123
+ }
124
+ await log("master_check_failed", `surface=${surface} alive=false`);
125
+ }
126
+ } catch (e: any) {
127
+ await log("master_check_error", e.message);
128
+ }
129
+
130
+ // Master spawn
131
+ const master = await spawnMaster(state.projectRoot, daemonSurface);
132
+ if (master) {
133
+ state.masterSurface = master.surface;
134
+ }
135
+ }
136
+
137
+ export async function initializeLayout(state: DaemonState, daemonSurface?: string): Promise<void> {
138
+ // team.json に既存 Conductor があり surface が生きていればスキップ
139
+ if (state.conductors.size > 0) {
140
+ const checks = await Promise.all(
141
+ [...state.conductors.values()].map(c => cmux.validateSurface(c.surface))
142
+ );
143
+ if (checks.some(alive => alive)) return;
144
+ }
145
+
146
+ const slots = await initializeConductorSlots(state.projectRoot, state.maxConductors, daemonSurface);
147
+ for (const slot of slots) {
148
+ state.conductors.set(slot.conductorId, slot);
149
+ }
150
+ }
151
+
152
+ export async function tick(state: DaemonState): Promise<void> {
153
+ state.lastUpdate = new Date();
154
+ await processQueue(state);
155
+ await scanTasks(state);
156
+ await monitorConductors(state);
157
+ }
158
+
159
+ async function processQueue(state: DaemonState): Promise<void> {
160
+ const messages = await readQueue();
161
+
162
+ for (const { path, message } of messages) {
163
+ switch (message.type) {
164
+ case "TASK_CREATED": {
165
+ let title = "";
166
+ if (message.taskFile && existsSync(message.taskFile)) {
167
+ try {
168
+ const content = await readFile(message.taskFile, "utf-8");
169
+ title = content.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? "";
170
+ } catch {}
171
+ }
172
+ await log("task_received", `task_id=${message.taskId}${title ? ` title=${title}` : ""}`);
173
+ break;
174
+ }
175
+
176
+ case "CONDUCTOR_DONE": {
177
+ const isSuccess = message.success !== false;
178
+ await log(
179
+ isSuccess ? "conductor_done_signal" : "conductor_error",
180
+ `conductor_id=${message.conductorId}${!isSuccess && message.reason ? ` reason=${message.reason}` : ""}${message.exitCode != null ? ` exit_code=${message.exitCode}` : ""}`
181
+ );
182
+ const conductor = findConductor(state, message.conductorId);
183
+ if (conductor) {
184
+ await handleConductorDone(state, conductor);
185
+ }
186
+ break;
187
+ }
188
+
189
+ case "AGENT_SPAWNED": {
190
+ const conductor = findConductor(state, message.conductorId);
191
+ if (conductor) {
192
+ conductor.agents.push({
193
+ surface: message.surface,
194
+ role: message.role,
195
+ taskTitle: message.taskTitle,
196
+ spawnedAt: message.timestamp,
197
+ });
198
+ await log(
199
+ "agent_spawned",
200
+ `conductor=${message.conductorId} surface=${message.surface}${message.role ? ` role=${message.role}` : ""}`
201
+ );
202
+ }
203
+ break;
204
+ }
205
+
206
+ case "AGENT_DONE": {
207
+ const conductor = findConductor(state, message.conductorId);
208
+ if (conductor) {
209
+ conductor.agents = conductor.agents.filter(
210
+ (a) => a.surface !== message.surface
211
+ );
212
+ await log(
213
+ "agent_done",
214
+ `conductor=${message.conductorId} surface=${message.surface}`
215
+ );
216
+ }
217
+ break;
218
+ }
219
+
220
+ case "SHUTDOWN":
221
+ await log("shutdown_requested");
222
+ state.running = false;
223
+ break;
224
+ }
225
+
226
+ await markProcessed(path);
227
+ }
228
+ }
229
+
230
+ async function scanTasks(state: DaemonState): Promise<void> {
231
+ const { tasks, taskState } = await loadTasks(state.projectRoot);
232
+
233
+ const closed = new Set(
234
+ Object.entries(taskState)
235
+ .filter(([_, s]) => s.status === "closed")
236
+ .map(([id]) => id)
237
+ );
238
+
239
+ const openTasksList = tasks.filter(t => t.status !== "closed");
240
+ state.openTasks = openTasksList.length;
241
+
242
+ const assignedIds = new Set(
243
+ [...state.conductors.values()].map((c) => c.taskId).filter((id): id is string => !!id)
244
+ );
245
+
246
+ const executable = sortByPriority(
247
+ filterExecutableTasks(openTasksList, closed, assignedIds)
248
+ );
249
+ state.pendingTasks = executable.length;
250
+
251
+ // taskList: open を優先表示、残り枠で closed(直近)を表示
252
+ const priorityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
253
+ const openTasks = [...openTasksList]
254
+ .sort((a, b) => (priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1));
255
+ const closedMetas = tasks.filter(t => t.status === "closed");
256
+ const closedTasks = [...closedMetas]
257
+ .sort((a, b) => (taskState[b.id]?.closedAt ?? "").localeCompare(taskState[a.id]?.closedAt ?? ""));
258
+ const maxItems = Math.max(5, openTasks.length);
259
+ const combined = [...openTasks, ...closedTasks.slice(0, maxItems - openTasks.length)];
260
+ state.taskList = combined.map((t) => ({
261
+ id: t.id,
262
+ title: t.title,
263
+ status: t.status,
264
+ createdAt: t.createdAt,
265
+ closedAt: taskState[t.id]?.closedAt,
266
+ }));
267
+
268
+ for (const task of executable) {
269
+ // idle Conductor を探す
270
+ const idleConductor = [...state.conductors.values()].find(c => c.status === "idle");
271
+ if (!idleConductor) {
272
+ await log("throttled", `task_id=${task.id} no_idle_conductor`);
273
+ break;
274
+ }
275
+
276
+ // spawn 前にロック(次の tick での二重起動を防止)
277
+ assignedIds.add(task.id);
278
+
279
+ const updated = await assignTask(idleConductor, task.id, state.projectRoot);
280
+ if (updated) {
281
+ state.conductors.set(updated.conductorId, updated);
282
+ }
283
+ }
284
+ }
285
+
286
+ async function monitorConductors(state: DaemonState): Promise<void> {
287
+ for (const [id, conductor] of state.conductors) {
288
+ if (conductor.status === "idle") continue;
289
+
290
+ if (conductor.status === "done") {
291
+ // 既に done 処理済み、surface 消失チェックのみ
292
+ if (!(await cmux.validateSurface(conductor.surface))) {
293
+ await log("conductor_surface_lost", `conductor_id=${id}`);
294
+ }
295
+ continue;
296
+ }
297
+
298
+ const status = await checkConductorStatus(conductor);
299
+
300
+ switch (status) {
301
+ case "done":
302
+ if (conductor.doneCandidate) {
303
+ await handleConductorDone(state, conductor);
304
+ } else {
305
+ conductor.doneCandidate = true;
306
+ }
307
+ break;
308
+ case "running":
309
+ conductor.doneCandidate = false;
310
+ break;
311
+ case "crashed":
312
+ await log(
313
+ "conductor_crashed",
314
+ `conductor_id=${id} surface=${conductor.surface}`
315
+ );
316
+ // persistent Conductor がクラッシュ → idle に戻す
317
+ conductor.status = "idle";
318
+ conductor.taskId = undefined;
319
+ break;
320
+ }
321
+ }
322
+ }
323
+
324
+ async function handleConductorDone(
325
+ state: DaemonState,
326
+ conductor: ConductorState
327
+ ): Promise<void> {
328
+ const { journalSummary } = await collectResults(conductor, state.projectRoot);
329
+
330
+ await log(
331
+ "task_completed",
332
+ `task_id=${conductor.taskId} conductor_id=${conductor.conductorId}${
333
+ conductor.taskTitle ? ` title=${conductor.taskTitle}` : ""
334
+ }${journalSummary ? ` journal_summary=${journalSummary}` : ""}`
335
+ );
336
+
337
+ // Conductor をリセットして idle に戻す
338
+ await resetConductor(conductor, state.projectRoot);
339
+ }
340
+
341
+ export async function updateTeamJson(state: DaemonState): Promise<void> {
342
+ const teamJsonPath = join(state.projectRoot, ".team/team.json");
343
+ try {
344
+ const teamJson = JSON.parse(await readFile(teamJsonPath, "utf-8"));
345
+ // master surface が null の場合は既存値を保持(reload 時に消さない)
346
+ if (state.masterSurface) {
347
+ teamJson.master = { surface: state.masterSurface };
348
+ }
349
+ teamJson.manager = {
350
+ pid: process.pid,
351
+ type: "typescript",
352
+ status: state.running ? "running" : "stopped",
353
+ };
354
+ teamJson.phase = "running";
355
+ teamJson.conductors = [...state.conductors.values()].map((c) => ({
356
+ id: c.conductorId,
357
+ taskRunId: c.taskRunId,
358
+ taskId: c.taskId,
359
+ taskTitle: c.taskTitle,
360
+ surface: c.surface,
361
+ status: c.status,
362
+ worktreePath: c.worktreePath,
363
+ outputDir: c.outputDir,
364
+ startedAt: c.startedAt,
365
+ paneId: c.paneId,
366
+ agents: c.agents.map((a) => ({
367
+ surface: a.surface,
368
+ role: a.role,
369
+ })),
370
+ }));
371
+ await writeFile(teamJsonPath, JSON.stringify(teamJson, null, 2) + "\n");
372
+ } catch {}
373
+ }