triflux 3.2.0-dev.7 → 3.2.0-dev.9

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 (42) hide show
  1. package/bin/triflux.mjs +557 -251
  2. package/hooks/keyword-rules.json +16 -0
  3. package/hub/bridge.mjs +410 -318
  4. package/hub/hitl.mjs +45 -31
  5. package/hub/pipe.mjs +457 -0
  6. package/hub/router.mjs +422 -161
  7. package/hub/server.mjs +429 -424
  8. package/hub/store.mjs +388 -314
  9. package/hub/team/cli-team-common.mjs +348 -0
  10. package/hub/team/cli-team-control.mjs +393 -0
  11. package/hub/team/cli-team-start.mjs +512 -0
  12. package/hub/team/cli-team-status.mjs +269 -0
  13. package/hub/team/cli.mjs +59 -1459
  14. package/hub/team/dashboard.mjs +1 -9
  15. package/hub/team/native.mjs +12 -80
  16. package/hub/team/nativeProxy.mjs +121 -47
  17. package/hub/team/pane.mjs +66 -43
  18. package/hub/team/psmux.mjs +297 -0
  19. package/hub/team/session.mjs +354 -291
  20. package/hub/team/shared.mjs +13 -0
  21. package/hub/team/staleState.mjs +299 -0
  22. package/hub/tools.mjs +41 -52
  23. package/hub/workers/claude-worker.mjs +446 -0
  24. package/hub/workers/codex-mcp.mjs +414 -0
  25. package/hub/workers/factory.mjs +18 -0
  26. package/hub/workers/gemini-worker.mjs +349 -0
  27. package/hub/workers/interface.mjs +41 -0
  28. package/hud/hud-qos-status.mjs +4 -2
  29. package/package.json +4 -1
  30. package/scripts/keyword-detector.mjs +15 -0
  31. package/scripts/lib/keyword-rules.mjs +4 -1
  32. package/scripts/psmux-steering-prototype.sh +368 -0
  33. package/scripts/setup.mjs +128 -70
  34. package/scripts/tfx-route-worker.mjs +161 -0
  35. package/scripts/tfx-route.sh +415 -80
  36. package/skills/tfx-auto/SKILL.md +90 -564
  37. package/skills/tfx-auto-codex/SKILL.md +1 -3
  38. package/skills/tfx-codex/SKILL.md +1 -4
  39. package/skills/tfx-doctor/SKILL.md +1 -0
  40. package/skills/tfx-gemini/SKILL.md +1 -4
  41. package/skills/tfx-setup/SKILL.md +1 -4
  42. package/skills/tfx-team/SKILL.md +53 -62
@@ -0,0 +1,269 @@
1
+ // hub/team/cli-team-status.mjs — team 상태/조회 로직
2
+ import {
3
+ capturePaneOutput,
4
+ detectMultiplexer,
5
+ getSessionAttachedCount,
6
+ hasWindowsTerminal,
7
+ hasWindowsTerminalSession,
8
+ listSessions,
9
+ } from "./session.mjs";
10
+ import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET, WHITE } from "./shared.mjs";
11
+ import {
12
+ TEAM_PROFILE,
13
+ getDefaultHubUrl,
14
+ getHubInfo,
15
+ isNativeMode,
16
+ isTeamAlive,
17
+ isWtMode,
18
+ loadTeamState,
19
+ nativeGetStatus,
20
+ ok,
21
+ saveTeamState,
22
+ } from "./cli-team-common.mjs";
23
+
24
+ async function fetchHubTaskList(state) {
25
+ const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
26
+ const teamName = state?.native?.teamName || state?.sessionName || null;
27
+ if (!teamName) return [];
28
+ try {
29
+ const res = await fetch(`${hubBase}/bridge/team/task-list`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ team_name: teamName }),
33
+ signal: AbortSignal.timeout(2000),
34
+ });
35
+ const data = await res.json();
36
+ return data?.ok ? (data.data?.tasks || []) : [];
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function renderTasks(tasks = []) {
43
+ if (!tasks.length) {
44
+ console.log(`\n ${DIM}태스크 없음${RESET}\n`);
45
+ return;
46
+ }
47
+
48
+ console.log(`\n ${AMBER}${BOLD}⬡ Team Tasks${RESET}\n`);
49
+ for (const t of tasks) {
50
+ const dep = t.depends_on?.length ? ` ${DIM}(deps: ${t.depends_on.join(",")})${RESET}` : "";
51
+ const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
52
+ console.log(` ${WHITE}${t.id}${RESET} ${t.status.padEnd(11)} ${t.title}${owner}${dep}`);
53
+ }
54
+ console.log("");
55
+ }
56
+
57
+ export async function teamStatus() {
58
+ const state = loadTeamState();
59
+ if (!state) {
60
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
61
+ return;
62
+ }
63
+
64
+ const alive = isTeamAlive(state);
65
+ const status = alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`;
66
+ const uptime = alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-";
67
+
68
+ console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET} ${status}\n`);
69
+ console.log(` 세션: ${state.sessionName}`);
70
+ console.log(` 모드: ${state.teammateMode || "tmux"}`);
71
+ console.log(` 리드: ${state.lead || "claude"}`);
72
+ console.log(` 워커: ${(state.agents || []).join(", ")}`);
73
+ console.log(` Uptime: ${uptime}`);
74
+ console.log(` 태스크: ${(state.tasks || []).length}`);
75
+ if (isWtMode(state) && !hasWindowsTerminalSession()) {
76
+ console.log(` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`);
77
+ }
78
+
79
+ const members = state.members || [];
80
+ if (members.length) {
81
+ console.log("");
82
+ for (const m of members) {
83
+ const roleTag = m.role === "lead" ? "lead" : "worker";
84
+ console.log(` - ${m.name} (${m.cli}) ${DIM}${roleTag}${RESET} ${DIM}${m.pane}${RESET}`);
85
+ }
86
+ }
87
+
88
+ if (isNativeMode(state) && alive) {
89
+ const native = await nativeGetStatus(state);
90
+ const nativeMembers = native?.data?.members || [];
91
+ if (nativeMembers.length) {
92
+ console.log("");
93
+ for (const m of nativeMembers) {
94
+ console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ if (alive) {
100
+ const hubTasks = await fetchHubTaskList(state);
101
+ if (hubTasks.length > 0) {
102
+ const completed = hubTasks.filter((t) => t.status === "completed").length;
103
+ const failed = hubTasks.filter((t) => t.status === "failed").length;
104
+
105
+ console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
106
+ for (const t of hubTasks) {
107
+ const icon = t.status === "completed" ? `${GREEN}✓${RESET}`
108
+ : t.status === "in_progress" ? `${AMBER}●${RESET}`
109
+ : t.status === "failed" ? `${RED}✗${RESET}`
110
+ : `${GRAY}○${RESET}`;
111
+ const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
112
+ const subject = t.subject || t.description?.slice(0, 50) || "";
113
+ console.log(` ${icon} ${subject}${owner}`);
114
+ }
115
+ if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
116
+ }
117
+ }
118
+
119
+ console.log("");
120
+ }
121
+
122
+ export function teamTasks() {
123
+ const state = loadTeamState();
124
+ if (!state || !isTeamAlive(state)) {
125
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
126
+ return;
127
+ }
128
+ renderTasks(state.tasks || []);
129
+ }
130
+
131
+ export function teamTaskUpdate() {
132
+ const state = loadTeamState();
133
+ if (!state || !isTeamAlive(state)) {
134
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
135
+ return;
136
+ }
137
+
138
+ const action = (process.argv[4] || "").toLowerCase();
139
+ const taskId = (process.argv[5] || "").toUpperCase();
140
+ const nextStatus = action === "done" || action === "complete" || action === "completed"
141
+ ? "completed"
142
+ : action === "progress" || action === "in-progress" || action === "in_progress"
143
+ ? "in_progress"
144
+ : action === "pending"
145
+ ? "pending"
146
+ : null;
147
+
148
+ if (!nextStatus || !taskId) {
149
+ console.log(`\n 사용법: ${WHITE}tfx team task <pending|progress|done> <T1>${RESET}\n`);
150
+ return;
151
+ }
152
+
153
+ const tasks = state.tasks || [];
154
+ const target = tasks.find((t) => String(t.id).toUpperCase() === taskId);
155
+ if (!target) {
156
+ console.log(`\n ${DIM}태스크를 찾을 수 없음: ${taskId}${RESET}\n`);
157
+ return;
158
+ }
159
+
160
+ target.status = nextStatus;
161
+ saveTeamState(state);
162
+ ok(`${target.id} 상태 갱신: ${nextStatus}`);
163
+ console.log("");
164
+ }
165
+
166
+ export async function teamDebug() {
167
+ const state = loadTeamState();
168
+ const linesIdx = process.argv.findIndex((a) => a === "--lines" || a === "-n");
169
+ const lines = linesIdx !== -1 ? Math.max(3, parseInt(process.argv[linesIdx + 1] || "20", 10) || 20) : 20;
170
+ const mux = detectMultiplexer() || "none";
171
+ const hub = await getHubInfo();
172
+
173
+ console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
174
+ console.log(` platform: ${process.platform}`);
175
+ console.log(` node: ${process.version}`);
176
+ console.log(` tty: stdout=${!!process.stdout.isTTY}, stdin=${!!process.stdin.isTTY}`);
177
+ console.log(` mux: ${mux}`);
178
+ console.log(` hub-pid: ${hub ? `${hub.pid}` : "-"}`);
179
+ console.log(` hub-url: ${hub?.url || "-"}`);
180
+
181
+ const sessions = listSessions();
182
+ console.log(` sessions: ${sessions.length ? sessions.join(", ") : "-"}`);
183
+
184
+ if (!state) {
185
+ console.log(`\n ${DIM}team-state 없음 (활성 세션 없음)${RESET}\n`);
186
+ return;
187
+ }
188
+
189
+ console.log(`\n ${BOLD}state${RESET}`);
190
+ console.log(` session: ${state.sessionName}`);
191
+ console.log(` profile: ${state.profile || TEAM_PROFILE}`);
192
+ console.log(` mode: ${state.teammateMode || "tmux"}`);
193
+ console.log(` lead: ${state.lead}`);
194
+ console.log(` agents: ${(state.agents || []).join(", ")}`);
195
+ console.log(` alive: ${isTeamAlive(state) ? "yes" : "no"}`);
196
+ const attached = getSessionAttachedCount(state.sessionName);
197
+ console.log(` attached: ${attached == null ? "-" : attached}`);
198
+
199
+ if (isWtMode(state)) {
200
+ const wtState = state.wt || {};
201
+ console.log(`\n ${BOLD}wt-session${RESET}`);
202
+ console.log(` window: ${wtState.windowId ?? 0}`);
203
+ console.log(` layout: ${wtState.layout || state.layout || "-"}`);
204
+ console.log(` panes: ${wtState.paneCount ?? (state.members || []).length}`);
205
+ console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
206
+ console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
207
+ console.log("");
208
+ return;
209
+ }
210
+
211
+ if (isNativeMode(state)) {
212
+ const native = await nativeGetStatus(state);
213
+ const members = native?.data?.members || [];
214
+ console.log(`\n ${BOLD}native-members${RESET}`);
215
+ if (!members.length) {
216
+ console.log(` ${DIM}(no data)${RESET}`);
217
+ } else {
218
+ for (const m of members) {
219
+ console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
220
+ }
221
+ }
222
+ console.log("");
223
+ return;
224
+ }
225
+
226
+ const members = state.members || [];
227
+ console.log(`\n ${BOLD}pane-tail${RESET} ${DIM}(last ${lines} lines)${RESET}`);
228
+ if (!members.length) {
229
+ console.log(` ${DIM}(members 없음)${RESET}`);
230
+ } else {
231
+ for (const m of members) {
232
+ const tail = capturePaneOutput(m.pane, lines) || "(empty)";
233
+ console.log(`\n [${m.name}] ${m.pane}`);
234
+ const tailLines = tail.split("\n").slice(-lines);
235
+ for (const line of tailLines) {
236
+ console.log(` ${line}`);
237
+ }
238
+ }
239
+ }
240
+ console.log("");
241
+ }
242
+
243
+ export function teamList() {
244
+ const state = loadTeamState();
245
+ if (state && isNativeMode(state) && isTeamAlive(state)) {
246
+ console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
247
+ console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(in-process)${RESET}`);
248
+ console.log("");
249
+ return;
250
+ }
251
+ if (state && isWtMode(state) && isTeamAlive(state)) {
252
+ console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
253
+ console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(wt)${RESET}`);
254
+ console.log("");
255
+ return;
256
+ }
257
+
258
+ const sessions = listSessions();
259
+ if (sessions.length === 0) {
260
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
261
+ return;
262
+ }
263
+ console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
264
+ for (const sessionName of sessions) {
265
+ console.log(` ${GREEN}●${RESET} ${sessionName}`);
266
+ }
267
+ console.log("");
268
+ }
269
+