triflux 3.2.0-dev.8 → 3.3.0-dev.1
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/bin/triflux.mjs +1296 -1055
- package/hooks/hooks.json +17 -0
- package/hooks/keyword-rules.json +20 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +517 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +422 -161
- package/hub/schema.sql +14 -0
- package/hub/server.mjs +499 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +75 -1475
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +190 -130
- package/hub/team/nativeProxy.mjs +165 -78
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +137 -103
- package/hub/team/psmux.mjs +506 -0
- package/hub/team/session.mjs +393 -330
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +105 -31
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1790 -1788
- package/package.json +4 -1
- package/scripts/__tests__/keyword-detector.test.mjs +8 -8
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +136 -71
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +485 -91
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +378 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -304
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// hub/team/cli-team-control.mjs — 팀 제어/attach/send 로직
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
attachSession,
|
|
6
|
+
resolveAttachCommand,
|
|
7
|
+
killSession,
|
|
8
|
+
closeWtSession,
|
|
9
|
+
sessionExists,
|
|
10
|
+
getSessionAttachedCount,
|
|
11
|
+
listSessions,
|
|
12
|
+
focusPane,
|
|
13
|
+
focusWtPane,
|
|
14
|
+
hasWindowsTerminal,
|
|
15
|
+
} from "./session.mjs";
|
|
16
|
+
import { injectPrompt, sendKeys } from "./pane.mjs";
|
|
17
|
+
import {
|
|
18
|
+
PKG_ROOT,
|
|
19
|
+
DIM,
|
|
20
|
+
RESET,
|
|
21
|
+
WHITE,
|
|
22
|
+
loadTeamState,
|
|
23
|
+
clearTeamState,
|
|
24
|
+
resolveMember,
|
|
25
|
+
publishLeadControl,
|
|
26
|
+
isNativeMode,
|
|
27
|
+
isWtMode,
|
|
28
|
+
isTeamAlive,
|
|
29
|
+
nativeRequest,
|
|
30
|
+
ok,
|
|
31
|
+
warn,
|
|
32
|
+
fail,
|
|
33
|
+
} from "./cli-team-common.mjs";
|
|
34
|
+
|
|
35
|
+
async function launchAttachInWindowsTerminal(sessionName) {
|
|
36
|
+
if (!hasWindowsTerminal()) return false;
|
|
37
|
+
|
|
38
|
+
let attachSpec;
|
|
39
|
+
try {
|
|
40
|
+
attachSpec = resolveAttachCommand(sessionName);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const launch = (args) => {
|
|
46
|
+
const child = spawn("wt", args, {
|
|
47
|
+
detached: true,
|
|
48
|
+
stdio: "ignore",
|
|
49
|
+
windowsHide: false,
|
|
50
|
+
});
|
|
51
|
+
child.unref();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const beforeAttached = getSessionAttachedCount(sessionName);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
launch(["-w", "0", "split-pane", "-V", "-d", PKG_ROOT, attachSpec.command, ...attachSpec.args]);
|
|
58
|
+
if (beforeAttached == null) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const deadline = Date.now() + 3500;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
65
|
+
const nowAttached = getSessionAttachedCount(sessionName);
|
|
66
|
+
if (typeof nowAttached === "number" && nowAttached > beforeAttached) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildManualAttachCommand(sessionName) {
|
|
77
|
+
try {
|
|
78
|
+
const spec = resolveAttachCommand(sessionName);
|
|
79
|
+
const quoted = [spec.command, ...spec.args].map((s) => {
|
|
80
|
+
const value = String(s);
|
|
81
|
+
return /\s/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
82
|
+
});
|
|
83
|
+
return quoted.join(" ");
|
|
84
|
+
} catch {
|
|
85
|
+
return `tmux attach-session -t ${sessionName}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function wantsWtAttachFallback() {
|
|
90
|
+
return process.argv.includes("--wt")
|
|
91
|
+
|| process.argv.includes("--spawn-wt")
|
|
92
|
+
|| process.env.TFX_ATTACH_WT_AUTO === "1";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function teamAttach() {
|
|
96
|
+
const state = loadTeamState();
|
|
97
|
+
if (!state || !isTeamAlive(state)) {
|
|
98
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isNativeMode(state)) {
|
|
103
|
+
console.log(`\n ${DIM}in-process 모드는 별도 attach가 없습니다.${RESET}`);
|
|
104
|
+
console.log(` ${DIM}상태 확인: tfx multi status${RESET}\n`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isWtMode(state)) {
|
|
109
|
+
console.log(`\n ${DIM}wt 모드는 attach 개념이 없습니다 (Windows Terminal pane가 독립 실행됨).${RESET}`);
|
|
110
|
+
console.log(` ${DIM}재실행/정리는: tfx multi stop${RESET}\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
attachSession(state.sessionName);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
const allowWt = wantsWtAttachFallback();
|
|
118
|
+
if (allowWt && await launchAttachInWindowsTerminal(state.sessionName)) {
|
|
119
|
+
warn(`현재 터미널에서 attach 실패: ${e.message}`);
|
|
120
|
+
ok("Windows Terminal split-pane로 attach 재시도 창을 열었습니다.");
|
|
121
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
122
|
+
console.log("");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
fail(`attach 실패: ${e.message}`);
|
|
126
|
+
if (allowWt) {
|
|
127
|
+
fail("WT 분할창 attach 자동 검증 실패 (session_attached 증가 없음)");
|
|
128
|
+
} else {
|
|
129
|
+
warn("자동 WT 분할은 기본 비활성입니다. 필요 시 --wt 옵션으로 실행하세요.");
|
|
130
|
+
}
|
|
131
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
132
|
+
console.log("");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function teamFocus() {
|
|
137
|
+
const state = loadTeamState();
|
|
138
|
+
if (!state || !isTeamAlive(state)) {
|
|
139
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isNativeMode(state)) {
|
|
144
|
+
console.log(`\n ${DIM}in-process 모드는 focus/attach 개념이 없습니다.${RESET}`);
|
|
145
|
+
console.log(` ${DIM}직접 지시: tfx multi send <대상> "메시지"${RESET}\n`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const selector = process.argv[4];
|
|
150
|
+
const member = resolveMember(state, selector);
|
|
151
|
+
if (!member) {
|
|
152
|
+
console.log(`\n 사용법: ${WHITE}tfx multi focus <lead|이름|번호>${RESET}\n`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isWtMode(state)) {
|
|
157
|
+
const match = /^wt:(\d+)$/.exec(member.pane || "");
|
|
158
|
+
const paneIndex = match ? parseInt(match[1], 10) : NaN;
|
|
159
|
+
if (!Number.isFinite(paneIndex)) {
|
|
160
|
+
warn(`wt pane 인덱스 파싱 실패: ${member.pane}`);
|
|
161
|
+
console.log("");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const focused = focusWtPane(paneIndex, {
|
|
165
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
166
|
+
});
|
|
167
|
+
if (focused) {
|
|
168
|
+
ok(`${member.name} pane 포커스 이동 (wt)`);
|
|
169
|
+
} else {
|
|
170
|
+
warn("wt pane 포커스 이동 실패 (WT_SESSION/wt.exe 상태 확인 필요)");
|
|
171
|
+
}
|
|
172
|
+
console.log("");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
focusPane(member.pane, { zoom: false });
|
|
177
|
+
try {
|
|
178
|
+
attachSession(state.sessionName);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
const allowWt = wantsWtAttachFallback();
|
|
181
|
+
if (allowWt && await launchAttachInWindowsTerminal(state.sessionName)) {
|
|
182
|
+
warn(`현재 터미널에서 attach 실패: ${e.message}`);
|
|
183
|
+
ok("Windows Terminal split-pane로 attach 재시도 창을 열었습니다.");
|
|
184
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
185
|
+
console.log("");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
fail(`attach 실패: ${e.message}`);
|
|
189
|
+
if (allowWt) {
|
|
190
|
+
fail("WT 분할창 attach 자동 검증 실패 (session_attached 증가 없음)");
|
|
191
|
+
} else {
|
|
192
|
+
warn("자동 WT 분할은 기본 비활성입니다. 필요 시 --wt 옵션으로 실행하세요.");
|
|
193
|
+
}
|
|
194
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
195
|
+
console.log("");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function teamInterrupt() {
|
|
200
|
+
const state = loadTeamState();
|
|
201
|
+
if (!state || !isTeamAlive(state)) {
|
|
202
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const selector = process.argv[4] || "lead";
|
|
207
|
+
const member = resolveMember(state, selector);
|
|
208
|
+
if (!member) {
|
|
209
|
+
console.log(`\n 사용법: ${WHITE}tfx multi interrupt <lead|이름|번호>${RESET}\n`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isWtMode(state)) {
|
|
214
|
+
warn("wt 모드에서는 pane stdin 주입이 지원되지 않아 interrupt를 자동 전송할 수 없습니다.");
|
|
215
|
+
console.log(` ${DIM}수동으로 해당 pane에서 Ctrl+C를 입력하세요.${RESET}`);
|
|
216
|
+
console.log("");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isNativeMode(state)) {
|
|
221
|
+
const result = await nativeRequest(state, "/interrupt", { member: member.name });
|
|
222
|
+
if (result?.ok) {
|
|
223
|
+
ok(`${member.name} 인터럽트 전송`);
|
|
224
|
+
} else {
|
|
225
|
+
warn(`${member.name} 인터럽트 실패`);
|
|
226
|
+
}
|
|
227
|
+
console.log("");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
sendKeys(member.pane, "C-c");
|
|
232
|
+
ok(`${member.name} 인터럽트 전송`);
|
|
233
|
+
console.log("");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function teamControl() {
|
|
237
|
+
const state = loadTeamState();
|
|
238
|
+
if (!state || !isTeamAlive(state)) {
|
|
239
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const selector = process.argv[4];
|
|
244
|
+
const command = (process.argv[5] || "").toLowerCase();
|
|
245
|
+
const reason = process.argv.slice(6).join(" ");
|
|
246
|
+
const member = resolveMember(state, selector);
|
|
247
|
+
const allowed = new Set(["interrupt", "stop", "pause", "resume"]);
|
|
248
|
+
|
|
249
|
+
if (!member || !allowed.has(command)) {
|
|
250
|
+
console.log(`\n 사용법: ${WHITE}tfx multi control <lead|이름|번호> <interrupt|stop|pause|resume> [사유]${RESET}\n`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (isWtMode(state)) {
|
|
255
|
+
warn("wt 모드는 Hub direct/control 주입 경로가 비활성입니다.");
|
|
256
|
+
console.log(` ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}`);
|
|
257
|
+
console.log("");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let directOk = false;
|
|
262
|
+
if (isNativeMode(state)) {
|
|
263
|
+
const direct = await nativeRequest(state, "/control", {
|
|
264
|
+
member: member.name,
|
|
265
|
+
command,
|
|
266
|
+
reason,
|
|
267
|
+
});
|
|
268
|
+
directOk = !!direct?.ok;
|
|
269
|
+
} else {
|
|
270
|
+
const controlMsg = `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`;
|
|
271
|
+
injectPrompt(member.pane, controlMsg);
|
|
272
|
+
if (command === "interrupt") {
|
|
273
|
+
sendKeys(member.pane, "C-c");
|
|
274
|
+
}
|
|
275
|
+
directOk = true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const published = await publishLeadControl(state, member, command, reason);
|
|
279
|
+
|
|
280
|
+
if (directOk && published) {
|
|
281
|
+
ok(`${member.name} 제어 전송 (${command}, direct + hub)`);
|
|
282
|
+
} else if (directOk) {
|
|
283
|
+
ok(`${member.name} 제어 전송 (${command}, direct only)`);
|
|
284
|
+
} else {
|
|
285
|
+
warn(`${member.name} 제어 전송 실패 (${command})`);
|
|
286
|
+
}
|
|
287
|
+
console.log("");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function teamStop() {
|
|
291
|
+
const state = loadTeamState();
|
|
292
|
+
if (!state) {
|
|
293
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (isNativeMode(state)) {
|
|
298
|
+
await nativeRequest(state, "/stop", {});
|
|
299
|
+
try {
|
|
300
|
+
process.kill(state.native.supervisorPid, "SIGTERM");
|
|
301
|
+
} catch {}
|
|
302
|
+
ok(`세션 종료: ${state.sessionName}`);
|
|
303
|
+
} else if (isWtMode(state)) {
|
|
304
|
+
const closed = closeWtSession({
|
|
305
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
306
|
+
paneCount: state?.wt?.paneCount ?? (state.members || []).length,
|
|
307
|
+
});
|
|
308
|
+
ok(`세션 종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
|
|
309
|
+
} else if (sessionExists(state.sessionName)) {
|
|
310
|
+
killSession(state.sessionName);
|
|
311
|
+
ok(`세션 종료: ${state.sessionName}`);
|
|
312
|
+
} else {
|
|
313
|
+
console.log(` ${DIM}세션 이미 종료됨${RESET}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
clearTeamState();
|
|
317
|
+
console.log("");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function teamKill() {
|
|
321
|
+
const state = loadTeamState();
|
|
322
|
+
if (state && isNativeMode(state) && isTeamAlive(state)) {
|
|
323
|
+
await nativeRequest(state, "/stop", {});
|
|
324
|
+
try {
|
|
325
|
+
process.kill(state.native.supervisorPid, "SIGTERM");
|
|
326
|
+
} catch {}
|
|
327
|
+
clearTeamState();
|
|
328
|
+
ok(`종료: ${state.sessionName}`);
|
|
329
|
+
console.log("");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (state && isWtMode(state)) {
|
|
334
|
+
const closed = closeWtSession({
|
|
335
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
336
|
+
paneCount: state?.wt?.paneCount ?? (state.members || []).length,
|
|
337
|
+
});
|
|
338
|
+
clearTeamState();
|
|
339
|
+
ok(`종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
|
|
340
|
+
console.log("");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const sessions = listSessions();
|
|
345
|
+
if (sessions.length === 0) {
|
|
346
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
for (const session of sessions) {
|
|
350
|
+
killSession(session);
|
|
351
|
+
ok(`종료: ${session}`);
|
|
352
|
+
}
|
|
353
|
+
clearTeamState();
|
|
354
|
+
console.log("");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function teamSend() {
|
|
358
|
+
const state = loadTeamState();
|
|
359
|
+
if (!state || !isTeamAlive(state)) {
|
|
360
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const selector = process.argv[4];
|
|
365
|
+
const message = process.argv.slice(5).join(" ");
|
|
366
|
+
const member = resolveMember(state, selector);
|
|
367
|
+
if (!member || !message) {
|
|
368
|
+
console.log(`\n 사용법: ${WHITE}tfx multi send <lead|이름|번호> "메시지"${RESET}\n`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (isWtMode(state)) {
|
|
373
|
+
warn("wt 모드는 pane 프롬프트 자동 주입(send)이 지원되지 않습니다.");
|
|
374
|
+
console.log(` ${DIM}수동 전달: 선택한 pane에 직접 붙여넣으세요.${RESET}`);
|
|
375
|
+
console.log("");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (isNativeMode(state)) {
|
|
380
|
+
const result = await nativeRequest(state, "/send", { member: member.name, text: message });
|
|
381
|
+
if (result?.ok) {
|
|
382
|
+
ok(`${member.name}에 메시지 주입 완료`);
|
|
383
|
+
} else {
|
|
384
|
+
warn(`${member.name} 메시지 주입 실패`);
|
|
385
|
+
}
|
|
386
|
+
console.log("");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
injectPrompt(member.pane, message);
|
|
391
|
+
ok(`${member.name}에 메시지 주입 완료`);
|
|
392
|
+
console.log("");
|
|
393
|
+
}
|