@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,550 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* E2E テストランナー
|
|
4
|
+
*
|
|
5
|
+
* 実際の cmux workspace で daemon + Master + Conductor を起動し、
|
|
6
|
+
* CLI 経由でタスクを投入してフルライフサイクルを検証する。
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ./e2e.ts [scenario]
|
|
10
|
+
*
|
|
11
|
+
* Scenarios:
|
|
12
|
+
* sequential — UC1: 順序付き依存実行 (調査→設計→実装)
|
|
13
|
+
* parallel — UC2: 並列調査 → 統合レポート
|
|
14
|
+
* all — 全シナリオ実行
|
|
15
|
+
*
|
|
16
|
+
* フロー:
|
|
17
|
+
* 1. 独立した cmux workspace を作成
|
|
18
|
+
* 2. daemon (main.ts start) を cmux send で起動 → dashboard + Master 自動 spawn
|
|
19
|
+
* 3. CLI (main.ts send) でタスクをキューに投入
|
|
20
|
+
* 4. manager.log で Conductor spawn/complete を監視
|
|
21
|
+
* 5. クリーンアップ
|
|
22
|
+
*
|
|
23
|
+
* 結果は .team/e2e-results/<timestamp>/ に保存される。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFile as execFileCb } from "child_process";
|
|
27
|
+
import { promisify } from "util";
|
|
28
|
+
import { mkdir, writeFile, readFile, readdir, rm, cp } from "fs/promises";
|
|
29
|
+
import { existsSync } from "fs";
|
|
30
|
+
import { join } from "path";
|
|
31
|
+
|
|
32
|
+
const execFile = promisify(execFileCb);
|
|
33
|
+
|
|
34
|
+
// --- 設定 ---
|
|
35
|
+
const SCRIPT_DIR = import.meta.dir;
|
|
36
|
+
const PROJECT_ROOT = join(SCRIPT_DIR, "../../..");
|
|
37
|
+
const TIMESTAMP = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
38
|
+
const RESULTS_BASE = join(PROJECT_ROOT, ".team/e2e-results");
|
|
39
|
+
const RESULTS_DIR = join(RESULTS_BASE, TIMESTAMP);
|
|
40
|
+
const WORKSPACE_DIR = join(RESULTS_BASE, "workspace");
|
|
41
|
+
|
|
42
|
+
// cmux リソース
|
|
43
|
+
let e2eWorkspace: string;
|
|
44
|
+
let daemonSurface: string;
|
|
45
|
+
let masterSurface: string;
|
|
46
|
+
|
|
47
|
+
let passed = 0;
|
|
48
|
+
let failed = 0;
|
|
49
|
+
const results: Array<{
|
|
50
|
+
scenario: string;
|
|
51
|
+
status: "pass" | "fail";
|
|
52
|
+
detail: string;
|
|
53
|
+
duration: number;
|
|
54
|
+
}> = [];
|
|
55
|
+
|
|
56
|
+
// --- ユーティリティ ---
|
|
57
|
+
|
|
58
|
+
function sleep(ms: number): Promise<void> {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function cmuxExec(...args: string[]): Promise<string> {
|
|
63
|
+
const { stdout } = await execFile("cmux", args, { timeout: 15_000 });
|
|
64
|
+
return stdout.trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** cmux send (--workspace 必須) */
|
|
68
|
+
async function cmuxSend(surface: string, text: string): Promise<void> {
|
|
69
|
+
await execFile("cmux", [
|
|
70
|
+
"send", "--workspace", e2eWorkspace, "--surface", surface, text,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function cmuxSendKey(surface: string, key: string): Promise<void> {
|
|
75
|
+
await execFile("cmux", [
|
|
76
|
+
"send-key", "--workspace", e2eWorkspace, "--surface", surface, key,
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function cmuxReadScreen(surface: string, lines: number = 15): Promise<string> {
|
|
81
|
+
const { stdout } = await execFile("cmux", [
|
|
82
|
+
"read-screen", "--workspace", e2eWorkspace, "--surface", surface,
|
|
83
|
+
"--lines", String(lines),
|
|
84
|
+
], { timeout: 10_000 });
|
|
85
|
+
return stdout;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readLog(): Promise<string> {
|
|
89
|
+
try {
|
|
90
|
+
return await readFile(join(WORKSPACE_DIR, ".team/logs/manager.log"), "utf-8");
|
|
91
|
+
} catch {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function waitForLog(pattern: string, timeoutMs: number = 180_000): Promise<boolean> {
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
while (Date.now() - start < timeoutMs) {
|
|
99
|
+
const log = await readLog();
|
|
100
|
+
if (log.includes(pattern)) return true;
|
|
101
|
+
await sleep(3000);
|
|
102
|
+
process.stdout.write(".");
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write("\n");
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readTeamJson(): Promise<any> {
|
|
109
|
+
return JSON.parse(await readFile(join(WORKSPACE_DIR, ".team/team.json"), "utf-8"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function captureSnapshot(label: string): Promise<void> {
|
|
113
|
+
const snapDir = join(RESULTS_DIR, "snapshots");
|
|
114
|
+
await mkdir(snapDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await writeFile(join(snapDir, `${label}-manager.log`), await readLog());
|
|
118
|
+
} catch {}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const qDir = join(WORKSPACE_DIR, ".team/queue/processed");
|
|
122
|
+
if (existsSync(qDir)) {
|
|
123
|
+
for (const f of await readdir(qDir)) {
|
|
124
|
+
await cp(join(qDir, f), join(snapDir, `${label}-queue-${f}`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await cp(join(WORKSPACE_DIR, ".team/team.json"), join(snapDir, `${label}-team.json`));
|
|
131
|
+
} catch {}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// task-state.json から closed タスクを特定してスナップショット
|
|
135
|
+
const stateFile = join(WORKSPACE_DIR, ".team/task-state.json");
|
|
136
|
+
if (existsSync(stateFile)) {
|
|
137
|
+
await cp(stateFile, join(snapDir, `${label}-task-state.json`));
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function assert(condition: boolean, message: string): void {
|
|
143
|
+
if (condition) {
|
|
144
|
+
console.log(` ✓ ${message}`);
|
|
145
|
+
passed++;
|
|
146
|
+
} else {
|
|
147
|
+
console.log(` ✗ ${message}`);
|
|
148
|
+
failed++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function countClosedTasks(): Promise<number> {
|
|
153
|
+
try {
|
|
154
|
+
const stateFile = join(WORKSPACE_DIR, ".team/task-state.json");
|
|
155
|
+
const state = JSON.parse(await readFile(stateFile, "utf-8"));
|
|
156
|
+
return Object.values(state).filter((s: any) => s.status === "closed").length;
|
|
157
|
+
} catch {
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** CLI でタスクファイルを作成 */
|
|
163
|
+
async function createTaskFile(
|
|
164
|
+
id: string,
|
|
165
|
+
slug: string,
|
|
166
|
+
opts: { priority?: string; dependsOn?: string[]; content?: string } = {}
|
|
167
|
+
): Promise<string> {
|
|
168
|
+
const { priority = "medium", dependsOn, content = "E2E テストタスク" } = opts;
|
|
169
|
+
let yaml = `---\nid: ${id}\ntitle: ${slug}\npriority: ${priority}\nstatus: ready\ncreated_at: ${new Date().toISOString()}\n`;
|
|
170
|
+
if (dependsOn?.length) yaml += `depends_on: [${dependsOn.join(", ")}]\n`;
|
|
171
|
+
yaml += `---\n\n## タスク\n${content}\n\n## 完了条件\n- 指示された成果物が作成されていること\n`;
|
|
172
|
+
|
|
173
|
+
const fileName = `${id.padStart(3, "0")}-${slug}.md`;
|
|
174
|
+
const filePath = join(WORKSPACE_DIR, `.team/tasks/${fileName}`);
|
|
175
|
+
await writeFile(filePath, yaml);
|
|
176
|
+
return filePath;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** CLI でキューにメッセージ送信 */
|
|
180
|
+
async function cliSend(...args: string[]): Promise<string> {
|
|
181
|
+
try {
|
|
182
|
+
const mainTs = join(WORKSPACE_DIR, ".team/manager/main.ts");
|
|
183
|
+
const { stdout } = await execFile("bun", ["run", mainTs, "send", ...args], {
|
|
184
|
+
cwd: WORKSPACE_DIR,
|
|
185
|
+
timeout: 30_000,
|
|
186
|
+
env: { ...process.env, PROJECT_ROOT: WORKSPACE_DIR },
|
|
187
|
+
});
|
|
188
|
+
return stdout.trim();
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
return e.stdout?.trim() || e.message;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- セットアップ ---
|
|
195
|
+
|
|
196
|
+
async function setup(): Promise<void> {
|
|
197
|
+
await rm(WORKSPACE_DIR, { recursive: true, force: true });
|
|
198
|
+
await mkdir(WORKSPACE_DIR, { recursive: true });
|
|
199
|
+
await mkdir(RESULTS_DIR, { recursive: true });
|
|
200
|
+
|
|
201
|
+
// .team 基本構造
|
|
202
|
+
for (const d of ["tasks", "queue/processed", "output", "prompts", "logs"]) {
|
|
203
|
+
await mkdir(join(WORKSPACE_DIR, `.team/${d}`), { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await writeFile(
|
|
207
|
+
join(WORKSPACE_DIR, ".team/team.json"),
|
|
208
|
+
JSON.stringify({ phase: "init", master: {}, manager: {}, conductors: [] }, null, 2)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// git init(worktree に必要)
|
|
212
|
+
await execFile("git", ["init"], { cwd: WORKSPACE_DIR });
|
|
213
|
+
await writeFile(join(WORKSPACE_DIR, "README.md"), "# E2E Test Project\n");
|
|
214
|
+
await execFile("git", ["add", "-A"], { cwd: WORKSPACE_DIR });
|
|
215
|
+
await execFile("git", ["commit", "-m", "init"], { cwd: WORKSPACE_DIR });
|
|
216
|
+
|
|
217
|
+
// manager ランタイムをコピー
|
|
218
|
+
const managerDst = join(WORKSPACE_DIR, ".team/manager");
|
|
219
|
+
await mkdir(managerDst, { recursive: true });
|
|
220
|
+
for (const f of [
|
|
221
|
+
"main.ts", "daemon.ts", "queue.ts", "schema.ts", "conductor.ts",
|
|
222
|
+
"master.ts", "cmux.ts", "template.ts", "logger.ts", "task.ts",
|
|
223
|
+
"dashboard.tsx", "package.json", "bun.lock", "tsconfig.json",
|
|
224
|
+
]) {
|
|
225
|
+
if (existsSync(join(SCRIPT_DIR, f))) {
|
|
226
|
+
await cp(join(SCRIPT_DIR, f), join(managerDst, f));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await execFile("bun", ["install"], { cwd: managerDst });
|
|
230
|
+
|
|
231
|
+
console.log(`\n${"═".repeat(60)}`);
|
|
232
|
+
console.log(` cmux-team E2E Test Runner`);
|
|
233
|
+
console.log(`${"═".repeat(60)}`);
|
|
234
|
+
console.log(` Workspace: ${WORKSPACE_DIR}`);
|
|
235
|
+
console.log(` Results: ${RESULTS_DIR}`);
|
|
236
|
+
console.log(` Time: ${new Date().toISOString()}`);
|
|
237
|
+
console.log(`${"═".repeat(60)}\n`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 独立 cmux workspace で daemon を起動。
|
|
242
|
+
* dashboard がそのペインに表示され、Master が new-split で自動 spawn される。
|
|
243
|
+
*/
|
|
244
|
+
async function startDaemon(): Promise<void> {
|
|
245
|
+
console.log("Creating workspace + starting daemon...");
|
|
246
|
+
|
|
247
|
+
// 1. 独立 workspace 作成
|
|
248
|
+
const wsOutput = await cmuxExec("new-workspace", "--cwd", WORKSPACE_DIR);
|
|
249
|
+
const workspaceMatch = wsOutput.match(/workspace:\d+/);
|
|
250
|
+
if (!workspaceMatch) throw new Error(`Failed to create workspace: ${wsOutput}`);
|
|
251
|
+
e2eWorkspace = workspaceMatch[0];
|
|
252
|
+
|
|
253
|
+
// workspace 内の surface を tree から取得
|
|
254
|
+
await sleep(3000);
|
|
255
|
+
const treeOutput = await cmuxExec("tree");
|
|
256
|
+
const wsRegex = new RegExp(`${e2eWorkspace}[\\s\\S]*?surface (surface:\\d+)`);
|
|
257
|
+
const surfaceMatch = treeOutput.match(wsRegex);
|
|
258
|
+
if (!surfaceMatch?.[1]) throw new Error(`Failed to find surface in ${e2eWorkspace}`);
|
|
259
|
+
daemonSurface = surfaceMatch[1];
|
|
260
|
+
console.log(` workspace: ${e2eWorkspace}, daemon surface: ${daemonSurface}`);
|
|
261
|
+
|
|
262
|
+
// 2. daemon 起動コマンドを送信(--workspace 指定必須)
|
|
263
|
+
const mainTs = join(WORKSPACE_DIR, ".team/manager/main.ts");
|
|
264
|
+
const cmd = `CMUX_TEAM_POLL_INTERVAL=5000 CMUX_TEAM_MAX_CONDUCTORS=3 PROJECT_ROOT=${WORKSPACE_DIR} bun run ${mainTs} start`;
|
|
265
|
+
await cmuxSend(daemonSurface, cmd + "\n");
|
|
266
|
+
|
|
267
|
+
// 3. daemon 起動を待つ
|
|
268
|
+
const daemonReady = await waitForLog("daemon_started", 30_000);
|
|
269
|
+
if (daemonReady) {
|
|
270
|
+
console.log(" daemon started ✓");
|
|
271
|
+
} else {
|
|
272
|
+
console.log(" WARNING: daemon 起動未確認。テストを続行します。");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 4. Master surface を team.json から取得(Master spawn + Trust 承認待ち)
|
|
276
|
+
await sleep(10_000);
|
|
277
|
+
try {
|
|
278
|
+
const team = await readTeamJson();
|
|
279
|
+
masterSurface = team.master?.surface;
|
|
280
|
+
if (masterSurface) {
|
|
281
|
+
console.log(` master surface: ${masterSurface}`);
|
|
282
|
+
} else {
|
|
283
|
+
console.log(" WARNING: Master surface が見つかりません(Master spawn に失敗した可能性)");
|
|
284
|
+
}
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
console.log(` WARNING: team.json 読み取り失敗: ${e.message}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function stopDaemon(): Promise<void> {
|
|
293
|
+
console.log("\nStopping daemon...");
|
|
294
|
+
|
|
295
|
+
// SHUTDOWN キューメッセージ
|
|
296
|
+
await cliSend("SHUTDOWN");
|
|
297
|
+
await sleep(5000);
|
|
298
|
+
|
|
299
|
+
// PID で確実に停止
|
|
300
|
+
try {
|
|
301
|
+
const team = await readTeamJson();
|
|
302
|
+
if (team.manager?.pid) {
|
|
303
|
+
process.kill(team.manager.pid, "SIGTERM");
|
|
304
|
+
}
|
|
305
|
+
} catch {}
|
|
306
|
+
|
|
307
|
+
// Conductor surface をクリーンアップ
|
|
308
|
+
try {
|
|
309
|
+
const team = await readTeamJson();
|
|
310
|
+
for (const c of team.conductors || []) {
|
|
311
|
+
await cmuxExec("close-surface", "--surface", c.surface).catch(() => {});
|
|
312
|
+
}
|
|
313
|
+
} catch {}
|
|
314
|
+
|
|
315
|
+
// Master surface をクリーンアップ
|
|
316
|
+
if (masterSurface) {
|
|
317
|
+
await cmuxExec("close-surface", "--surface", masterSurface).catch(() => {});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// daemon surface をクリーンアップ (Ctrl+C → close)
|
|
321
|
+
if (daemonSurface) {
|
|
322
|
+
try { await cmuxSendKey(daemonSurface, "C-c"); } catch {}
|
|
323
|
+
await sleep(2000);
|
|
324
|
+
await cmuxExec("close-surface", "--surface", daemonSurface).catch(() => {});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// git worktree クリーンアップ
|
|
328
|
+
try {
|
|
329
|
+
const worktreesDir = join(WORKSPACE_DIR, ".worktrees");
|
|
330
|
+
if (existsSync(worktreesDir)) {
|
|
331
|
+
const dirs = await readdir(worktreesDir);
|
|
332
|
+
for (const d of dirs) {
|
|
333
|
+
await execFile("git", ["worktree", "remove", join(worktreesDir, d), "--force"], {
|
|
334
|
+
cwd: WORKSPACE_DIR,
|
|
335
|
+
}).catch(() => {});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
await execFile("git", ["worktree", "prune"], { cwd: WORKSPACE_DIR }).catch(() => {});
|
|
339
|
+
} catch {}
|
|
340
|
+
|
|
341
|
+
console.log("daemon stopped ✓");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --- シナリオ 1: 順序付き実行 ---
|
|
345
|
+
|
|
346
|
+
async function scenarioSequential(): Promise<void> {
|
|
347
|
+
const start = Date.now();
|
|
348
|
+
console.log("━━━ Scenario 1: Sequential Dependencies (A→B→C) ━━━");
|
|
349
|
+
console.log(" 3 つのタスクを連鎖依存で実行:");
|
|
350
|
+
console.log(" Task 1 (調査) → Task 2 (設計) → Task 3 (実装)\n");
|
|
351
|
+
|
|
352
|
+
await createTaskFile("1", "research-api", {
|
|
353
|
+
priority: "high",
|
|
354
|
+
content: `API エンドポイントの一覧を調査し、.team/output/research-api.md に結果を書き出してください。
|
|
355
|
+
|
|
356
|
+
具体的には:
|
|
357
|
+
1. このプロジェクトの README.md を読む
|
|
358
|
+
2. 「API エンドポイント: /health, /users, /tasks」という内容で .team/output/research-api.md を作成
|
|
359
|
+
3. 完了`,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await createTaskFile("2", "design-schema", {
|
|
363
|
+
dependsOn: ["1"],
|
|
364
|
+
content: `.team/output/research-api.md を読み、データスキーマを設計し、.team/output/design-schema.md に書き出してください。
|
|
365
|
+
|
|
366
|
+
具体的には:
|
|
367
|
+
1. .team/output/research-api.md を読む
|
|
368
|
+
2. 各エンドポイントに対応するスキーマ定義を作成
|
|
369
|
+
3. .team/output/design-schema.md に書き出す`,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await createTaskFile("3", "implement-handler", {
|
|
373
|
+
dependsOn: ["2"],
|
|
374
|
+
content: `.team/output/design-schema.md を読み、handler.ts を実装してください。
|
|
375
|
+
|
|
376
|
+
具体的には:
|
|
377
|
+
1. .team/output/design-schema.md を読む
|
|
378
|
+
2. src/handler.ts を作成(簡単な Express ハンドラー)
|
|
379
|
+
3. 完了`,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await cliSend("TASK_CREATED", "--task-id", "1", "--task-file", ".team/tasks/001-research-api.md");
|
|
383
|
+
|
|
384
|
+
console.log(" Waiting for Task 1 (research)...");
|
|
385
|
+
const t1 = await waitForLog("task_completed task_id=1", 180_000);
|
|
386
|
+
assert(t1, "Task 1 (research) 完了");
|
|
387
|
+
|
|
388
|
+
if (t1) {
|
|
389
|
+
console.log(" Waiting for Task 2 (design)...");
|
|
390
|
+
const t2 = await waitForLog("task_completed task_id=2", 180_000);
|
|
391
|
+
assert(t2, "Task 2 (design) 依存解決 → 完了");
|
|
392
|
+
|
|
393
|
+
if (t2) {
|
|
394
|
+
console.log(" Waiting for Task 3 (implement)...");
|
|
395
|
+
const t3 = await waitForLog("task_completed task_id=3", 180_000);
|
|
396
|
+
assert(t3, "Task 3 (implement) 依存解決 → 完了");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const log = await readLog();
|
|
401
|
+
const events = log.split("\n")
|
|
402
|
+
.filter((l) => /conductor_started|task_completed/.test(l))
|
|
403
|
+
.map((l) => l.trim());
|
|
404
|
+
|
|
405
|
+
console.log("\n 実行ログ:");
|
|
406
|
+
events.forEach((e) => console.log(` ${e}`));
|
|
407
|
+
|
|
408
|
+
const closedCount = await countClosedTasks();
|
|
409
|
+
assert(closedCount >= 1, `${closedCount} タスクが closed に移動`);
|
|
410
|
+
|
|
411
|
+
await captureSnapshot("sequential");
|
|
412
|
+
|
|
413
|
+
results.push({
|
|
414
|
+
scenario: "sequential",
|
|
415
|
+
status: t1 ? "pass" : "fail",
|
|
416
|
+
detail: `${events.length} events, ${closedCount} closed`,
|
|
417
|
+
duration: Date.now() - start,
|
|
418
|
+
});
|
|
419
|
+
console.log();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- シナリオ 2: 並列調査 → 統合 ---
|
|
423
|
+
|
|
424
|
+
async function scenarioParallel(): Promise<void> {
|
|
425
|
+
const start = Date.now();
|
|
426
|
+
console.log("━━━ Scenario 2: Parallel Research → Consolidation ━━━");
|
|
427
|
+
console.log(" 3 つの調査を並列実行し、結果を統合:\n");
|
|
428
|
+
|
|
429
|
+
await createTaskFile("10", "research-frontend", {
|
|
430
|
+
content: `フロントエンド技術を調査し、.team/output/research-frontend.md に書き出してください。
|
|
431
|
+
|
|
432
|
+
具体的には:
|
|
433
|
+
1. 「React, Vue, Svelte の比較」という内容で .team/output/research-frontend.md を作成
|
|
434
|
+
2. 完了`,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await createTaskFile("11", "research-backend", {
|
|
438
|
+
content: `バックエンド技術を調査し、.team/output/research-backend.md に書き出してください。
|
|
439
|
+
|
|
440
|
+
具体的には:
|
|
441
|
+
1. 「Express, Fastify, Hono の比較」という内容で .team/output/research-backend.md を作成
|
|
442
|
+
2. 完了`,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await createTaskFile("12", "research-database", {
|
|
446
|
+
content: `データベース技術を調査し、.team/output/research-database.md に書き出してください。
|
|
447
|
+
|
|
448
|
+
具体的には:
|
|
449
|
+
1. 「PostgreSQL, MongoDB, SQLite の比較」という内容で .team/output/research-database.md を作成
|
|
450
|
+
2. 完了`,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
await createTaskFile("13", "consolidate-report", {
|
|
454
|
+
dependsOn: ["10", "11", "12"],
|
|
455
|
+
content: `.team/output/research-*.md を全て読み、統合レポートを作成してください。
|
|
456
|
+
|
|
457
|
+
具体的には:
|
|
458
|
+
1. .team/output/research-frontend.md, research-backend.md, research-database.md を読む
|
|
459
|
+
2. 統合レポートを .team/output/tech-stack-report.md に作成
|
|
460
|
+
3. 完了`,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await cliSend("TASK_CREATED", "--task-id", "10", "--task-file", ".team/tasks/010-research-frontend.md");
|
|
464
|
+
|
|
465
|
+
console.log(" Waiting for parallel spawns...");
|
|
466
|
+
const s10 = await waitForLog("conductor_started task_id=10", 60_000);
|
|
467
|
+
const s11 = await waitForLog("conductor_started task_id=11", 60_000);
|
|
468
|
+
const s12 = await waitForLog("conductor_started task_id=12", 60_000);
|
|
469
|
+
|
|
470
|
+
assert(s10, "Task 10 (frontend research) spawn");
|
|
471
|
+
assert(s11, "Task 11 (backend research) spawn");
|
|
472
|
+
assert(s12, "Task 12 (database research) spawn");
|
|
473
|
+
|
|
474
|
+
const logBefore = await readLog();
|
|
475
|
+
assert(!logBefore.includes("conductor_started task_id=13"), "Task 13 (consolidate) はまだブロック中");
|
|
476
|
+
|
|
477
|
+
console.log(" Waiting for all research to complete...");
|
|
478
|
+
await waitForLog("task_completed task_id=10", 180_000);
|
|
479
|
+
await waitForLog("task_completed task_id=11", 180_000);
|
|
480
|
+
await waitForLog("task_completed task_id=12", 180_000);
|
|
481
|
+
|
|
482
|
+
console.log(" Waiting for consolidation...");
|
|
483
|
+
const s13 = await waitForLog("conductor_started task_id=13", 120_000);
|
|
484
|
+
assert(s13, "全調査完了後に Task 13 (consolidate) が spawn された");
|
|
485
|
+
|
|
486
|
+
await captureSnapshot("parallel");
|
|
487
|
+
|
|
488
|
+
results.push({
|
|
489
|
+
scenario: "parallel",
|
|
490
|
+
status: s10 && s11 && s12 ? "pass" : "fail",
|
|
491
|
+
detail: `parallel: ${[s10, s11, s12].filter(Boolean).length}/3, consolidate: ${s13 ? "yes" : "no"}`,
|
|
492
|
+
duration: Date.now() - start,
|
|
493
|
+
});
|
|
494
|
+
console.log();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// --- メイン ---
|
|
498
|
+
|
|
499
|
+
async function main(): Promise<void> {
|
|
500
|
+
const scenario = process.argv[2] || "all";
|
|
501
|
+
|
|
502
|
+
if (!process.env.CMUX_SOCKET_PATH) {
|
|
503
|
+
console.log("⚠ cmux 環境外で実行中。");
|
|
504
|
+
console.log(" cmux 内で実行してください: cmux で起動したターミナルから ./e2e.ts\n");
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
await setup();
|
|
509
|
+
await startDaemon();
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
if (scenario === "all" || scenario === "sequential") await scenarioSequential();
|
|
513
|
+
if (scenario === "all" || scenario === "parallel") await scenarioParallel();
|
|
514
|
+
} finally {
|
|
515
|
+
await stopDaemon();
|
|
516
|
+
await captureSnapshot("final");
|
|
517
|
+
|
|
518
|
+
console.log(`\n${"═".repeat(60)}`);
|
|
519
|
+
console.log(` Results: ${passed} passed, ${failed} failed`);
|
|
520
|
+
console.log(`${"═".repeat(60)}`);
|
|
521
|
+
|
|
522
|
+
for (const r of results) {
|
|
523
|
+
const icon = r.status === "pass" ? "✓" : "✗";
|
|
524
|
+
console.log(` ${icon} ${r.scenario} (${Math.round(r.duration / 1000)}s): ${r.detail}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await writeFile(
|
|
528
|
+
join(RESULTS_DIR, "results.json"),
|
|
529
|
+
JSON.stringify({ passed, failed, results, timestamp: new Date().toISOString() }, null, 2)
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
console.log(`\n アーティファクト: ${RESULTS_DIR}/`);
|
|
533
|
+
console.log(` manager.log: ${RESULTS_DIR}/snapshots/final-manager.log`);
|
|
534
|
+
|
|
535
|
+
const finalLog = await readLog();
|
|
536
|
+
const sessions = [...finalLog.matchAll(/session=([a-f0-9-]+)/g)].map((m) => m[1]);
|
|
537
|
+
if (sessions.length > 0) {
|
|
538
|
+
console.log(`\n Conductor セッション(claude --resume で参照可能):`);
|
|
539
|
+
sessions.forEach((s) => console.log(` claude --resume ${s}`));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log();
|
|
543
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
main().catch((e) => {
|
|
548
|
+
console.error("E2E test runner crashed:", e);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const PROJECT_ROOT = process.env.PROJECT_ROOT || process.cwd();
|
|
5
|
+
const LOG_DIR = join(PROJECT_ROOT, ".team/logs");
|
|
6
|
+
const LOG_FILE = join(LOG_DIR, "manager.log");
|
|
7
|
+
|
|
8
|
+
export async function log(event: string, detail: string = ""): Promise<void> {
|
|
9
|
+
await mkdir(LOG_DIR, { recursive: true });
|
|
10
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
11
|
+
const line = `[${timestamp}] ${event} ${detail}`.trimEnd() + "\n";
|
|
12
|
+
await appendFile(LOG_FILE, line);
|
|
13
|
+
}
|