triflux 3.2.0-dev.1 → 3.2.0-dev.10

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 (53) hide show
  1. package/README.ko.md +26 -18
  2. package/README.md +26 -18
  3. package/bin/triflux.mjs +1614 -1084
  4. package/hooks/hooks.json +12 -0
  5. package/hooks/keyword-rules.json +354 -0
  6. package/hub/bridge.mjs +371 -193
  7. package/hub/hitl.mjs +45 -31
  8. package/hub/pipe.mjs +457 -0
  9. package/hub/router.mjs +422 -161
  10. package/hub/server.mjs +429 -344
  11. package/hub/store.mjs +388 -314
  12. package/hub/team/cli-team-common.mjs +348 -0
  13. package/hub/team/cli-team-control.mjs +393 -0
  14. package/hub/team/cli-team-start.mjs +516 -0
  15. package/hub/team/cli-team-status.mjs +269 -0
  16. package/hub/team/cli.mjs +99 -368
  17. package/hub/team/dashboard.mjs +165 -64
  18. package/hub/team/native-supervisor.mjs +300 -0
  19. package/hub/team/native.mjs +62 -0
  20. package/hub/team/nativeProxy.mjs +534 -0
  21. package/hub/team/orchestrator.mjs +99 -35
  22. package/hub/team/pane.mjs +138 -101
  23. package/hub/team/psmux.mjs +297 -0
  24. package/hub/team/session.mjs +608 -186
  25. package/hub/team/shared.mjs +13 -0
  26. package/hub/team/staleState.mjs +299 -0
  27. package/hub/tools.mjs +140 -53
  28. package/hub/workers/claude-worker.mjs +446 -0
  29. package/hub/workers/codex-mcp.mjs +414 -0
  30. package/hub/workers/factory.mjs +18 -0
  31. package/hub/workers/gemini-worker.mjs +349 -0
  32. package/hub/workers/interface.mjs +41 -0
  33. package/hud/hud-qos-status.mjs +1789 -1732
  34. package/package.json +6 -2
  35. package/scripts/__tests__/keyword-detector.test.mjs +234 -0
  36. package/scripts/hub-ensure.mjs +83 -0
  37. package/scripts/keyword-detector.mjs +272 -0
  38. package/scripts/keyword-rules-expander.mjs +521 -0
  39. package/scripts/lib/keyword-rules.mjs +168 -0
  40. package/scripts/psmux-steering-prototype.sh +368 -0
  41. package/scripts/run.cjs +62 -0
  42. package/scripts/setup.mjs +189 -7
  43. package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
  44. package/scripts/tfx-route-worker.mjs +161 -0
  45. package/scripts/tfx-route.sh +943 -508
  46. package/skills/tfx-auto/SKILL.md +90 -564
  47. package/skills/tfx-auto-codex/SKILL.md +77 -0
  48. package/skills/tfx-codex/SKILL.md +1 -4
  49. package/skills/tfx-doctor/SKILL.md +1 -0
  50. package/skills/tfx-gemini/SKILL.md +1 -4
  51. package/skills/tfx-multi/SKILL.md +296 -0
  52. package/skills/tfx-setup/SKILL.md +1 -4
  53. package/skills/tfx-team/SKILL.md +0 -172
@@ -1,46 +1,52 @@
1
1
  #!/usr/bin/env node
2
- // hub/team/dashboard.mjs — 실시간 팀 상태 표시
3
- // 실행: watch -n 1 -c 'node hub/team/dashboard.mjs --session tfx-team-xxx'
4
- // 또는: node hub/team/dashboard.mjs --session tfx-team-xxx (단일 출력)
2
+ // hub/team/dashboard.mjs — 실시간 팀 상태 표시 (v2.2)
3
+ // tmux 의존 제거 Hub task-list + native-supervisor 기반
4
+ //
5
+ // 실행:
6
+ // node hub/team/dashboard.mjs --session <세션이름> [--interval 2]
7
+ // node hub/team/dashboard.mjs --team <팀이름> [--interval 2]
5
8
  import { get } from "node:http";
6
- import { capturePaneOutput } from "./session.mjs";
7
-
8
- // ── 색상 ──
9
- const AMBER = "\x1b[38;5;214m";
10
- const GREEN = "\x1b[38;5;82m";
11
- const RED = "\x1b[38;5;196m";
12
- const GRAY = "\x1b[38;5;245m";
13
- const DIM = "\x1b[2m";
14
- const BOLD = "\x1b[1m";
15
- const RESET = "\x1b[0m";
9
+ import { AMBER, GREEN, RED, GRAY, DIM, BOLD, RESET } from "./shared.mjs";
16
10
 
17
11
  /**
18
- * Hub /status 엔드포인트 조회
19
- * @param {string} hubUrl — 예: http://127.0.0.1:27888
12
+ * HTTP GET JSON
13
+ * @param {string} url
20
14
  * @returns {Promise<object|null>}
21
15
  */
22
- function fetchStatus(hubUrl) {
16
+ function fetchJson(url) {
23
17
  return new Promise((resolve) => {
24
- const url = `${hubUrl}/status`;
25
18
  const req = get(url, { timeout: 2000 }, (res) => {
26
19
  let data = "";
27
20
  res.on("data", (chunk) => (data += chunk));
28
21
  res.on("end", () => {
29
- try {
30
- resolve(JSON.parse(data));
31
- } catch {
32
- resolve(null);
33
- }
22
+ try { resolve(JSON.parse(data)); } catch { resolve(null); }
34
23
  });
35
24
  });
36
25
  req.on("error", () => resolve(null));
37
- req.on("timeout", () => {
38
- req.destroy();
39
- resolve(null);
40
- });
26
+ req.on("timeout", () => { req.destroy(); resolve(null); });
41
27
  });
42
28
  }
43
29
 
30
+ /**
31
+ * HTTP POST JSON (Hub bridge 용)
32
+ * @param {string} url
33
+ * @param {object} body
34
+ * @returns {Promise<object|null>}
35
+ */
36
+ async function fetchPost(url, body = {}) {
37
+ try {
38
+ const res = await fetch(url, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify(body),
42
+ signal: AbortSignal.timeout(2000),
43
+ });
44
+ return await res.json();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
44
50
  /**
45
51
  * 진행률 바 생성
46
52
  * @param {number} pct — 0~100
@@ -65,56 +71,141 @@ function formatUptime(ms) {
65
71
  }
66
72
 
67
73
  /**
68
- * 대시보드 렌더링
69
- * @param {string} sessionName — tmux 세션 이름
74
+ * task 상태 아이콘
75
+ * @param {string} status
76
+ * @returns {string}
77
+ */
78
+ function statusIcon(status) {
79
+ switch (status) {
80
+ case "completed": return `${GREEN}✓${RESET}`;
81
+ case "in_progress": return `${AMBER}●${RESET}`;
82
+ case "failed": return `${RED}✗${RESET}`;
83
+ default: return `${GRAY}○${RESET}`;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 멤버 목록 구성: Hub tasks + supervisor + teamState 통합
89
+ * @param {Array} hubTasks — Hub bridge task-list 결과
90
+ * @param {Array} supervisorMembers — native-supervisor 멤버 상태
91
+ * @param {object} teamState — team-state.json 내용
92
+ * @returns {Array<{name: string, cli: string, status: string, subject: string, preview: string}>}
93
+ */
94
+ function buildMemberList(hubTasks, supervisorMembers, teamState) {
95
+ const members = [];
96
+ const supervisorByName = new Map(supervisorMembers.map((m) => [m.name, m]));
97
+
98
+ // Hub tasks가 있으면 주 데이터 소스
99
+ if (hubTasks.length > 0) {
100
+ for (const task of hubTasks) {
101
+ const owner = task.owner || task.subject || "";
102
+ const sup = supervisorByName.get(owner);
103
+ members.push({
104
+ name: owner,
105
+ cli: task.metadata?.cli || sup?.cli || "",
106
+ status: task.status || "pending",
107
+ subject: task.subject || "",
108
+ preview: sup?.lastPreview || task.description?.slice(0, 80) || "",
109
+ });
110
+ }
111
+ return members;
112
+ }
113
+
114
+ // Supervisor 데이터 폴백
115
+ if (supervisorMembers.length > 0) {
116
+ for (const m of supervisorMembers) {
117
+ if (m.role === "lead") continue;
118
+ members.push({
119
+ name: m.name,
120
+ cli: m.cli || "",
121
+ status: m.status === "running" ? "in_progress" : m.status === "exited" ? "completed" : m.status,
122
+ subject: "",
123
+ preview: m.lastPreview || "",
124
+ });
125
+ }
126
+ return members;
127
+ }
128
+
129
+ // teamState 폴백 (하위 호환)
130
+ const panes = teamState?.panes || {};
131
+ for (const [, paneInfo] of Object.entries(panes).filter(([, v]) => v.role !== "dashboard" && v.role !== "lead")) {
132
+ members.push({
133
+ name: paneInfo.agentId || paneInfo.name || "?",
134
+ cli: paneInfo.cli || "",
135
+ status: "unknown",
136
+ subject: paneInfo.subtask || "",
137
+ preview: "",
138
+ });
139
+ }
140
+ return members;
141
+ }
142
+
143
+ /**
144
+ * 대시보드 렌더링 (v2.2: Hub/supervisor 기반)
145
+ * @param {string} sessionName — 세션 또는 팀 이름
70
146
  * @param {object} opts
71
147
  * @param {string} opts.hubUrl — Hub URL (기본 http://127.0.0.1:27888)
72
- * @param {object} opts.teamStateteam-state.json 내용
148
+ * @param {string} [opts.teamName]Hub task-list 조회용 팀 이름
149
+ * @param {string} [opts.supervisorUrl] — native-supervisor 제어 URL
150
+ * @param {object} [opts.teamState] — team-state.json 내용 (하위 호환)
73
151
  */
74
152
  export async function renderDashboard(sessionName, opts = {}) {
75
- const { hubUrl = "http://127.0.0.1:27888", teamState = {} } = opts;
153
+ const {
154
+ hubUrl = "http://127.0.0.1:27888",
155
+ teamName,
156
+ supervisorUrl,
157
+ teamState = {},
158
+ } = opts;
76
159
  const W = 50;
77
160
  const border = "─".repeat(W);
78
161
 
79
- // Hub 상태 조회
80
- const status = await fetchStatus(hubUrl);
81
- const hubOnline = !!status;
162
+ // 데이터 수집 (병렬)
163
+ const [hubStatus, taskListRes, supervisorRes] = await Promise.all([
164
+ fetchJson(`${hubUrl}/status`),
165
+ teamName ? fetchPost(`${hubUrl}/bridge/team/task-list`, { team_name: teamName }) : null,
166
+ supervisorUrl ? fetchJson(`${supervisorUrl}/status`) : null,
167
+ ]);
168
+
169
+ const hubOnline = !!hubStatus;
82
170
  const hubState = hubOnline ? `${GREEN}● online${RESET}` : `${RED}● offline${RESET}`;
83
- const uptime = status?.hub?.uptime ? formatUptime(status.hub.uptime) : "-";
84
- const queueSize = status?.hub?.queue_depth ?? 0;
171
+ const uptime = hubStatus?.hub?.uptime ? formatUptime(hubStatus.hub.uptime) : "-";
172
+ const queueSize = hubStatus?.hub?.queue_depth ?? 0;
173
+
174
+ // Hub task 데이터
175
+ const hubTasks = taskListRes?.ok ? (taskListRes.data?.tasks || []) : [];
176
+ const completedCount = hubTasks.filter((t) => t.status === "completed").length;
177
+ const totalCount = hubTasks.length;
178
+
179
+ // Supervisor 멤버 데이터
180
+ const supervisorMembers = supervisorRes?.ok ? (supervisorRes.data?.members || []) : [];
85
181
 
86
182
  // 헤더
87
- console.log(`${AMBER}┌─ ${sessionName} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - 3))}${AMBER}┐${RESET}`);
183
+ const progress = totalCount > 0 ? ` ${completedCount}/${totalCount}` : "";
184
+ console.log(`${AMBER}┌─ ${sessionName}${progress} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - progress.length - 3))}${AMBER}┐${RESET}`);
88
185
  console.log(`${AMBER}│${RESET} Hub: ${hubState} Uptime: ${DIM}${uptime}${RESET} Queue: ${DIM}${queueSize}${RESET}`);
89
186
  console.log(`${AMBER}│${RESET}`);
90
187
 
91
- // 에이전트 상태
92
- const panes = teamState?.panes || {};
93
- const paneEntries = Object.entries(panes).filter(([, v]) => v.role !== "dashboard");
188
+ // 멤버/워커 렌더링
189
+ const members = buildMemberList(hubTasks, supervisorMembers, teamState);
94
190
 
95
- if (paneEntries.length === 0) {
191
+ if (members.length === 0) {
96
192
  console.log(`${AMBER}│${RESET} ${DIM}에이전트 정보 없음${RESET}`);
97
193
  } else {
98
- for (const [paneTarget, paneInfo] of paneEntries) {
99
- const { cli = "?", agentId = "?", subtask = "" } = paneInfo;
100
- const label = `[${agentId}]`;
101
- const cliTag = cli.charAt(0).toUpperCase() + cli.slice(1);
102
-
103
- // Hub에서 에이전트 상태 확인
104
- const agentStatus = status?.agents?.find?.((a) => a.agent_id === agentId);
105
- const online = agentStatus ? `${GREEN}● online${RESET}` : `${GRAY}○ -${RESET}`;
106
-
107
- // 진행률 추정 (메시지 기반, 단순 휴리스틱)
108
- const msgCount = agentStatus?.message_count ?? 0;
109
- const pct = msgCount === 0 ? 0 : Math.min(100, msgCount * 25);
110
-
111
- console.log(`${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${online} ${progressBar(pct)} ${DIM}${pct}%${RESET}`);
112
-
113
- // pane 미리보기 (마지막 2줄)
114
- const preview = capturePaneOutput(paneTarget, 2)
115
- .split("\n")
116
- .filter(Boolean)
117
- .slice(-1)[0] || "";
194
+ for (const m of members) {
195
+ const icon = statusIcon(m.status);
196
+ const label = `[${m.name}]`;
197
+ const cliTag = m.cli ? m.cli.charAt(0).toUpperCase() + m.cli.slice(1) : "";
198
+
199
+ // 진행률 추정
200
+ const pct = m.status === "completed" ? 100
201
+ : m.status === "in_progress" ? 50
202
+ : m.status === "failed" ? 100
203
+ : 0;
204
+
205
+ console.log(`${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${icon} ${m.status || "pending"} ${progressBar(pct)}`);
206
+
207
+ // 미리보기: supervisor lastPreview > task subject
208
+ const preview = m.preview || m.subject || "";
118
209
  if (preview) {
119
210
  const truncated = preview.length > W - 8 ? preview.slice(0, W - 11) + "..." : preview;
120
211
  console.log(`${AMBER}│${RESET} ${DIM}> ${truncated}${RESET}`);
@@ -140,14 +231,17 @@ async function loadTeamState() {
140
231
  }
141
232
  }
142
233
 
143
- // ── CLI 실행 (자체 갱신 루프 — watch 불필요) ──
234
+ // ── CLI 실행 ──
144
235
  if (process.argv[1]?.includes("dashboard.mjs")) {
145
236
  const sessionIdx = process.argv.indexOf("--session");
237
+ const teamIdx = process.argv.indexOf("--team");
146
238
  const sessionName = sessionIdx !== -1 ? process.argv[sessionIdx + 1] : null;
239
+ const teamName = teamIdx !== -1 ? process.argv[teamIdx + 1] : null;
147
240
  const intervalSec = parseInt(process.argv[process.argv.indexOf("--interval") + 1] || "2", 10);
148
241
 
149
- if (!sessionName) {
150
- console.error("사용법: node dashboard.mjs --session <세션이름> [--interval 2]");
242
+ const displayName = sessionName || teamName;
243
+ if (!displayName) {
244
+ console.error("사용법: node dashboard.mjs --session <세션이름> [--team <팀이름>] [--interval 2]");
151
245
  process.exit(1);
152
246
  }
153
247
 
@@ -157,9 +251,16 @@ if (process.argv[1]?.includes("dashboard.mjs")) {
157
251
  // 갱신 루프
158
252
  while (true) {
159
253
  const teamState = await loadTeamState();
254
+ const effectiveTeamName = teamName || null;
255
+ const supervisorUrl = teamState?.native?.controlUrl || null;
256
+
160
257
  // 화면 클리어 (ANSI)
161
258
  process.stdout.write("\x1b[2J\x1b[H");
162
- await renderDashboard(sessionName, { teamState });
259
+ await renderDashboard(displayName, {
260
+ teamName: effectiveTeamName,
261
+ supervisorUrl,
262
+ teamState,
263
+ });
163
264
  console.log(`${DIM} ${intervalSec}초 간격 갱신 | Ctrl+C로 종료${RESET}`);
164
265
  await new Promise((r) => setTimeout(r, intervalSec * 1000));
165
266
  }
@@ -0,0 +1,300 @@
1
+ // hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
2
+ import { createServer } from "node:http";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+
7
+ function parseArgs(argv) {
8
+ const out = {};
9
+ for (let i = 0; i < argv.length; i++) {
10
+ const cur = argv[i];
11
+ if (cur === "--config" && argv[i + 1]) {
12
+ out.config = argv[++i];
13
+ }
14
+ }
15
+ return out;
16
+ }
17
+
18
+ async function readJson(path) {
19
+ return JSON.parse(readFileSync(path, "utf8"));
20
+ }
21
+
22
+ function safeText(v, fallback = "") {
23
+ if (v == null) return fallback;
24
+ return String(v);
25
+ }
26
+
27
+ function nowMs() {
28
+ return Date.now();
29
+ }
30
+
31
+ const args = parseArgs(process.argv.slice(2));
32
+ if (!args.config) {
33
+ console.error("사용법: node native-supervisor.mjs --config <path>");
34
+ process.exit(1);
35
+ }
36
+
37
+ const config = await readJson(args.config);
38
+ const {
39
+ sessionName,
40
+ runtimeFile,
41
+ logsDir,
42
+ startupDelayMs = 3000,
43
+ members = [],
44
+ } = config;
45
+
46
+ mkdirSync(logsDir, { recursive: true });
47
+ mkdirSync(dirname(runtimeFile), { recursive: true });
48
+
49
+ const startedAt = nowMs();
50
+ const processMap = new Map();
51
+
52
+ function memberStateSnapshot() {
53
+ const states = [];
54
+ for (const m of members) {
55
+ const state = processMap.get(m.name);
56
+ states.push({
57
+ name: m.name,
58
+ role: m.role,
59
+ cli: m.cli,
60
+ agentId: m.agentId,
61
+ command: m.command,
62
+ pid: state?.child?.pid || null,
63
+ status: state?.status || "unknown",
64
+ exitCode: state?.exitCode ?? null,
65
+ lastPreview: state?.lastPreview || "",
66
+ logFile: state?.logFile || null,
67
+ errFile: state?.errFile || null,
68
+ });
69
+ }
70
+ return states;
71
+ }
72
+
73
+ function writeRuntime(controlPort) {
74
+ const runtime = {
75
+ sessionName,
76
+ supervisorPid: process.pid,
77
+ controlUrl: `http://127.0.0.1:${controlPort}`,
78
+ startedAt,
79
+ members: memberStateSnapshot(),
80
+ };
81
+ writeFileSync(runtimeFile, JSON.stringify(runtime, null, 2) + "\n");
82
+ }
83
+
84
+ function spawnMember(member) {
85
+ const outPath = join(logsDir, `${member.name}.out.log`);
86
+ const errPath = join(logsDir, `${member.name}.err.log`);
87
+
88
+ const outWs = createWriteStream(outPath, { flags: "a" });
89
+ const errWs = createWriteStream(errPath, { flags: "a" });
90
+
91
+ const child = spawn(member.command, {
92
+ shell: true,
93
+ env: {
94
+ ...process.env,
95
+ TERM: process.env.TERM && process.env.TERM !== "dumb" ? process.env.TERM : "xterm-256color",
96
+ },
97
+ stdio: ["pipe", "pipe", "pipe"],
98
+ windowsHide: true,
99
+ });
100
+
101
+ const state = {
102
+ member,
103
+ child,
104
+ outWs,
105
+ errWs,
106
+ logFile: outPath,
107
+ errFile: errPath,
108
+ status: "running",
109
+ exitCode: null,
110
+ lastPreview: "",
111
+ };
112
+
113
+ child.stdout.on("data", (buf) => {
114
+ outWs.write(buf);
115
+ const txt = safeText(buf).trim();
116
+ if (txt) {
117
+ const lines = txt.split(/\r?\n/).filter(Boolean);
118
+ if (lines.length) state.lastPreview = lines[lines.length - 1].slice(0, 280);
119
+ }
120
+ });
121
+
122
+ child.stderr.on("data", (buf) => {
123
+ errWs.write(buf);
124
+ const txt = safeText(buf).trim();
125
+ if (txt) {
126
+ const lines = txt.split(/\r?\n/).filter(Boolean);
127
+ if (lines.length) state.lastPreview = `[err] ${lines[lines.length - 1].slice(0, 260)}`;
128
+ }
129
+ });
130
+
131
+ child.on("exit", (code) => {
132
+ state.status = "exited";
133
+ state.exitCode = code;
134
+ try { outWs.end(); } catch {}
135
+ try { errWs.end(); } catch {}
136
+ maybeAutoShutdown();
137
+ });
138
+
139
+ processMap.set(member.name, state);
140
+ }
141
+
142
+ function sendInput(memberName, text) {
143
+ const state = processMap.get(memberName);
144
+ if (!state) return { ok: false, error: "member_not_found" };
145
+ if (state.status !== "running") return { ok: false, error: "member_not_running" };
146
+ try {
147
+ state.child.stdin.write(`${safeText(text)}\n`);
148
+ return { ok: true };
149
+ } catch (e) {
150
+ return { ok: false, error: e.message };
151
+ }
152
+ }
153
+
154
+ function interruptMember(memberName) {
155
+ const state = processMap.get(memberName);
156
+ if (!state) return { ok: false, error: "member_not_found" };
157
+ if (state.status !== "running") return { ok: false, error: "member_not_running" };
158
+
159
+ let signaled = false;
160
+ try {
161
+ signaled = state.child.kill("SIGINT");
162
+ } catch {
163
+ signaled = false;
164
+ }
165
+
166
+ if (!signaled) {
167
+ try {
168
+ state.child.stdin.write("\u0003");
169
+ signaled = true;
170
+ } catch {
171
+ signaled = false;
172
+ }
173
+ }
174
+
175
+ return signaled ? { ok: true } : { ok: false, error: "interrupt_failed" };
176
+ }
177
+
178
+ let isShuttingDown = false;
179
+
180
+ function maybeAutoShutdown() {
181
+ if (isShuttingDown) return;
182
+ const allExited = [...processMap.values()].every((s) => s.status === "exited");
183
+ if (!allExited) return;
184
+ shutdown();
185
+ }
186
+
187
+ function shutdown() {
188
+ if (isShuttingDown) return;
189
+ isShuttingDown = true;
190
+
191
+ for (const state of processMap.values()) {
192
+ if (state.status === "running") {
193
+ try { state.child.stdin.write("exit\n"); } catch {}
194
+ try { state.child.kill("SIGTERM"); } catch {}
195
+ }
196
+ try { state.outWs.end(); } catch {}
197
+ try { state.errWs.end(); } catch {}
198
+ }
199
+
200
+ setTimeout(() => {
201
+ for (const state of processMap.values()) {
202
+ if (state.status === "running") {
203
+ try { state.child.kill("SIGKILL"); } catch {}
204
+ }
205
+ }
206
+ process.exit(0);
207
+ }, 1200).unref();
208
+ }
209
+
210
+ for (const member of members) {
211
+ spawnMember(member);
212
+ }
213
+
214
+ const server = createServer(async (req, res) => {
215
+ const send = (code, obj) => {
216
+ res.writeHead(code, { "Content-Type": "application/json" });
217
+ res.end(JSON.stringify(obj));
218
+ };
219
+
220
+ if (req.method === "GET" && (req.url === "/" || req.url === "/status")) {
221
+ return send(200, {
222
+ ok: true,
223
+ data: {
224
+ sessionName,
225
+ supervisorPid: process.pid,
226
+ uptimeMs: nowMs() - startedAt,
227
+ members: memberStateSnapshot(),
228
+ },
229
+ });
230
+ }
231
+
232
+ if (req.method !== "POST") {
233
+ return send(405, { ok: false, error: "method_not_allowed" });
234
+ }
235
+
236
+ let body = {};
237
+ try {
238
+ const chunks = [];
239
+ for await (const c of req) chunks.push(c);
240
+ const raw = Buffer.concat(chunks).toString("utf8") || "{}";
241
+ body = JSON.parse(raw);
242
+ } catch {
243
+ return send(400, { ok: false, error: "invalid_json" });
244
+ }
245
+
246
+ if (req.url === "/send") {
247
+ const { member, text } = body;
248
+ const r = sendInput(member, text);
249
+ return send(r.ok ? 200 : 400, r);
250
+ }
251
+
252
+ if (req.url === "/interrupt") {
253
+ const { member } = body;
254
+ const r = interruptMember(member);
255
+ return send(r.ok ? 200 : 400, r);
256
+ }
257
+
258
+ if (req.url === "/control") {
259
+ const { member, command = "", reason = "" } = body;
260
+ const controlMsg = `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`;
261
+ const a = sendInput(member, controlMsg);
262
+ if (!a.ok) return send(400, a);
263
+ if (String(command).toLowerCase() === "interrupt") {
264
+ const b = interruptMember(member);
265
+ if (!b.ok) return send(400, b);
266
+ }
267
+ return send(200, { ok: true });
268
+ }
269
+
270
+ if (req.url === "/stop") {
271
+ send(200, { ok: true });
272
+ shutdown();
273
+ return;
274
+ }
275
+
276
+ return send(404, { ok: false, error: "not_found" });
277
+ });
278
+
279
+ server.listen(0, "127.0.0.1", () => {
280
+ const address = server.address();
281
+ const port = typeof address === "object" && address ? address.port : null;
282
+ if (!port) {
283
+ console.error("native supervisor 포트 바인딩 실패");
284
+ process.exit(1);
285
+ }
286
+
287
+ writeRuntime(port);
288
+
289
+ // CLI 초기화 후 프롬프트 주입
290
+ setTimeout(() => {
291
+ for (const m of members) {
292
+ if (m.prompt) {
293
+ sendInput(m.name, m.prompt);
294
+ }
295
+ }
296
+ }, startupDelayMs).unref();
297
+ });
298
+
299
+ process.on("SIGINT", shutdown);
300
+ process.on("SIGTERM", shutdown);
@@ -0,0 +1,62 @@
1
+ // hub/team/native.mjs — Claude Native Teams 래퍼
2
+ // teammate 프롬프트 템플릿 + 팀 설정 빌더
3
+ //
4
+ // Claude Code 네이티브 Agent Teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)
5
+ // 환경에서 teammate를 Codex/Gemini CLI 래퍼로 구성하는 유틸리티.
6
+ // SKILL.md가 인라인 프롬프트를 사용하므로, 이 모듈은 CLI(tfx multi --native)에서
7
+ // 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
8
+
9
+ const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
10
+
11
+ /**
12
+ * v2.2 슬림 래퍼 프롬프트 생성
13
+ * Agent spawn으로 네비게이션에 등록하되, 실제 작업은 tfx-route.sh가 수행.
14
+ * 프롬프트 ~100 토큰 목표 (v2의 ~500 대비 80% 감소).
15
+ *
16
+ * @param {'codex'|'gemini'} cli — CLI 타입
17
+ * @param {object} opts
18
+ * @param {string} opts.subtask — 서브태스크 설명
19
+ * @param {string} [opts.role] — 역할 (executor, designer, reviewer 등)
20
+ * @param {string} [opts.teamName] — 팀 이름
21
+ * @param {string} [opts.taskId] — Hub task ID
22
+ * @param {string} [opts.agentName] — 워커 표시 이름
23
+ * @param {string} [opts.leadName] — 리드 수신자 이름
24
+ * @param {string} [opts.mcp_profile] — MCP 프로필
25
+ * @returns {string} 슬림 래퍼 프롬프트
26
+ */
27
+ export function buildSlimWrapperPrompt(cli, opts = {}) {
28
+ const {
29
+ subtask,
30
+ role = "executor",
31
+ teamName = "tfx-multi",
32
+ taskId = "",
33
+ agentName = "",
34
+ leadName = "team-lead",
35
+ mcp_profile = "auto",
36
+ } = opts;
37
+
38
+ // 셸 이스케이프
39
+ const escaped = subtask.replace(/'/g, "'\\''");
40
+
41
+ return `Bash 1회 실행 후 반드시 종료하라. 어떤 경우에도 hang하지 마라.
42
+ gemini/codex를 직접 호출하지 마라. 반드시 tfx-route.sh를 거쳐야 한다.
43
+ 프롬프트를 파일로 저장하지 마라. tfx-route.sh가 인자로 받는다.
44
+
45
+ TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}" bash ${ROUTE_SCRIPT} "${role}" '${escaped}' ${mcp_profile}
46
+
47
+ 성공 → TaskUpdate(status: completed, metadata: {result: "success"}) + SendMessage(to: ${leadName}).
48
+ 실패 → TaskUpdate(status: completed, metadata: {result: "failed", error: "에러 요약"}) + SendMessage(to: ${leadName}).
49
+
50
+ 중요: TaskUpdate의 status는 "completed"만 사용. "failed"는 API 미지원.
51
+ 실패 여부는 metadata.result로 구분. Bash 실패 시에도 반드시 TaskUpdate + SendMessage 후 종료.`;
52
+ }
53
+
54
+ /**
55
+ * 팀 이름 생성 (타임스탬프 기반)
56
+ * @returns {string}
57
+ */
58
+ export function generateTeamName() {
59
+ const ts = Date.now().toString(36).slice(-4);
60
+ const rand = Math.random().toString(36).slice(2, 6);
61
+ return `tfx-${ts}${rand}`;
62
+ }