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,348 @@
|
|
|
1
|
+
// hub/team/cli-team-common.mjs — team CLI 공통 상태/Hub 유틸
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
resolveAttachCommand,
|
|
9
|
+
sessionExists,
|
|
10
|
+
getSessionAttachedCount,
|
|
11
|
+
hasWindowsTerminal,
|
|
12
|
+
hasWindowsTerminalSession,
|
|
13
|
+
} from "./session.mjs";
|
|
14
|
+
import { AMBER, GREEN, RED, GRAY, DIM, BOLD, RESET, WHITE, YELLOW } from "./shared.mjs";
|
|
15
|
+
|
|
16
|
+
export { AMBER, GREEN, RED, GRAY, DIM, BOLD, RESET, WHITE, YELLOW };
|
|
17
|
+
|
|
18
|
+
export const PKG_ROOT = dirname(dirname(dirname(new URL(import.meta.url).pathname))).replace(/^\/([A-Z]:)/, "$1");
|
|
19
|
+
export const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
20
|
+
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
21
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
22
|
+
export const TEAM_PROFILE = (() => {
|
|
23
|
+
const raw = String(process.env.TFX_TEAM_PROFILE || "team").trim().toLowerCase();
|
|
24
|
+
return raw === "codex-team" ? "codex-team" : "team";
|
|
25
|
+
})();
|
|
26
|
+
const TEAM_STATE_FILE = join(
|
|
27
|
+
HUB_PID_DIR,
|
|
28
|
+
TEAM_PROFILE === "codex-team" ? "team-state-codex-team.json" : "team-state.json",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const TEAM_SUBCOMMANDS = new Set([
|
|
32
|
+
"status", "attach", "stop", "kill", "send", "list", "help", "tasks", "task", "focus", "interrupt", "control", "debug",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
|
|
36
|
+
export function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
37
|
+
export function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
|
|
38
|
+
|
|
39
|
+
export function loadTeamState() {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(TEAM_STATE_FILE, "utf8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function saveTeamState(state) {
|
|
48
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
49
|
+
const nextState = { ...state, profile: TEAM_PROFILE };
|
|
50
|
+
writeFileSync(TEAM_STATE_FILE, JSON.stringify(nextState, null, 2) + "\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearTeamState() {
|
|
54
|
+
try { unlinkSync(TEAM_STATE_FILE); } catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatHostForUrl(host) {
|
|
58
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildHubBaseUrl(host, port) {
|
|
62
|
+
return `http://${formatHostForUrl(host)}:${port}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getDefaultHubPort() {
|
|
66
|
+
const envPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
67
|
+
return Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : 27888;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getDefaultHubUrl() {
|
|
71
|
+
return `${buildHubBaseUrl("127.0.0.1", getDefaultHubPort())}/mcp`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeLoopbackHost(host) {
|
|
75
|
+
if (typeof host !== "string") return "127.0.0.1";
|
|
76
|
+
const candidate = host.trim();
|
|
77
|
+
return LOOPBACK_HOSTS.has(candidate) ? candidate : "127.0.0.1";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function probeHubStatus(host, port, timeoutMs = 1500) {
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
|
|
83
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) return null;
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
return data?.hub ? data : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function getHubInfo() {
|
|
94
|
+
const probePort = getDefaultHubPort();
|
|
95
|
+
|
|
96
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
99
|
+
const pid = Number(raw?.pid);
|
|
100
|
+
if (!Number.isFinite(pid) || pid <= 0) throw new Error("invalid pid");
|
|
101
|
+
process.kill(pid, 0);
|
|
102
|
+
const host = normalizeLoopbackHost(raw?.host);
|
|
103
|
+
const port = Number(raw.port) || 27888;
|
|
104
|
+
const status = await probeHubStatus(host, port, 1200);
|
|
105
|
+
if (!status) {
|
|
106
|
+
return {
|
|
107
|
+
...raw,
|
|
108
|
+
pid,
|
|
109
|
+
host,
|
|
110
|
+
port,
|
|
111
|
+
url: `${buildHubBaseUrl(host, port)}/mcp`,
|
|
112
|
+
degraded: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
...raw,
|
|
117
|
+
pid,
|
|
118
|
+
host,
|
|
119
|
+
port,
|
|
120
|
+
url: `${buildHubBaseUrl(host, port)}/mcp`,
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const candidates = Array.from(new Set([probePort, 27888]));
|
|
128
|
+
for (const portCandidate of candidates) {
|
|
129
|
+
const data = await probeHubStatus("127.0.0.1", portCandidate, 1200);
|
|
130
|
+
if (!data) continue;
|
|
131
|
+
const port = Number(data.port) || portCandidate;
|
|
132
|
+
const pid = Number(data.pid);
|
|
133
|
+
const recovered = {
|
|
134
|
+
pid: Number.isFinite(pid) ? pid : null,
|
|
135
|
+
host: "127.0.0.1",
|
|
136
|
+
port,
|
|
137
|
+
url: `${buildHubBaseUrl("127.0.0.1", port)}/mcp`,
|
|
138
|
+
discovered: true,
|
|
139
|
+
};
|
|
140
|
+
if (Number.isFinite(recovered.pid) && recovered.pid > 0) {
|
|
141
|
+
try {
|
|
142
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
143
|
+
writeFileSync(HUB_PID_FILE, JSON.stringify({
|
|
144
|
+
pid: recovered.pid,
|
|
145
|
+
port: recovered.port,
|
|
146
|
+
host: recovered.host,
|
|
147
|
+
url: recovered.url,
|
|
148
|
+
started: Date.now(),
|
|
149
|
+
}));
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
return recovered;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function startHubDaemon() {
|
|
158
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
159
|
+
if (!existsSync(serverPath)) {
|
|
160
|
+
fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음");
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
165
|
+
env: { ...process.env },
|
|
166
|
+
stdio: "ignore",
|
|
167
|
+
detached: true,
|
|
168
|
+
});
|
|
169
|
+
child.unref();
|
|
170
|
+
|
|
171
|
+
const expectedPort = getDefaultHubPort();
|
|
172
|
+
const deadline = Date.now() + 3000;
|
|
173
|
+
while (Date.now() < deadline) {
|
|
174
|
+
const status = await probeHubStatus("127.0.0.1", expectedPort, 500);
|
|
175
|
+
if (status?.hub) {
|
|
176
|
+
return {
|
|
177
|
+
pid: Number(status.pid) || child.pid,
|
|
178
|
+
host: "127.0.0.1",
|
|
179
|
+
port: expectedPort,
|
|
180
|
+
url: `${buildHubBaseUrl("127.0.0.1", expectedPort)}/mcp`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function isNativeMode(state) {
|
|
190
|
+
return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function isWtMode(state) {
|
|
194
|
+
return state?.teammateMode === "wt";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function isTeamAlive(state) {
|
|
198
|
+
if (!state) return false;
|
|
199
|
+
if (isNativeMode(state)) {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(state.native.supervisorPid, 0);
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (isWtMode(state)) {
|
|
208
|
+
if (!hasWindowsTerminal()) return false;
|
|
209
|
+
if (hasWindowsTerminalSession()) return true;
|
|
210
|
+
return Array.isArray(state.members) && state.members.length > 0;
|
|
211
|
+
}
|
|
212
|
+
return sessionExists(state.sessionName);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function nativeRequest(state, path, body = {}) {
|
|
216
|
+
if (!isNativeMode(state)) return null;
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetch(`${state.native.controlUrl}${path}`, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: { "Content-Type": "application/json" },
|
|
221
|
+
body: JSON.stringify(body),
|
|
222
|
+
});
|
|
223
|
+
return await res.json();
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function nativeGetStatus(state) {
|
|
230
|
+
if (!isNativeMode(state)) return null;
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch(`${state.native.controlUrl}/status`);
|
|
233
|
+
return await res.json();
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function launchAttachInWindowsTerminal(sessionName) {
|
|
240
|
+
if (!hasWindowsTerminal()) return false;
|
|
241
|
+
|
|
242
|
+
let attachSpec;
|
|
243
|
+
try {
|
|
244
|
+
attachSpec = resolveAttachCommand(sessionName);
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const launch = (args) => {
|
|
250
|
+
const child = spawn("wt", args, {
|
|
251
|
+
detached: true,
|
|
252
|
+
stdio: "ignore",
|
|
253
|
+
windowsHide: false,
|
|
254
|
+
});
|
|
255
|
+
child.unref();
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const beforeAttached = getSessionAttachedCount(sessionName);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
launch(["-w", "0", "split-pane", "-V", "-d", PKG_ROOT, attachSpec.command, ...attachSpec.args]);
|
|
262
|
+
if (beforeAttached == null) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const deadline = Date.now() + 3500;
|
|
267
|
+
while (Date.now() < deadline) {
|
|
268
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
269
|
+
const nowAttached = getSessionAttachedCount(sessionName);
|
|
270
|
+
if (typeof nowAttached === "number" && nowAttached > beforeAttached) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function buildManualAttachCommand(sessionName) {
|
|
281
|
+
try {
|
|
282
|
+
const spec = resolveAttachCommand(sessionName);
|
|
283
|
+
const quoted = [spec.command, ...spec.args].map((s) => {
|
|
284
|
+
const v = String(s);
|
|
285
|
+
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
286
|
+
});
|
|
287
|
+
return quoted.join(" ");
|
|
288
|
+
} catch {
|
|
289
|
+
return `tmux attach-session -t ${sessionName}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function wantsWtAttachFallback() {
|
|
294
|
+
return process.argv.includes("--wt")
|
|
295
|
+
|| process.argv.includes("--spawn-wt")
|
|
296
|
+
|| process.env.TFX_ATTACH_WT_AUTO === "1";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function resolveMember(state, selector) {
|
|
300
|
+
const members = state?.members || [];
|
|
301
|
+
if (!selector) return null;
|
|
302
|
+
|
|
303
|
+
const direct = members.find((m) => m.name === selector || m.role === selector || m.agentId === selector);
|
|
304
|
+
if (direct) return direct;
|
|
305
|
+
|
|
306
|
+
const workerAlias = /^worker-(\d+)$/i.exec(selector);
|
|
307
|
+
if (workerAlias) {
|
|
308
|
+
const workerIdx = parseInt(workerAlias[1], 10) - 1;
|
|
309
|
+
const workers = members.filter((m) => m.role === "worker");
|
|
310
|
+
if (workerIdx >= 0 && workerIdx < workers.length) return workers[workerIdx];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const n = parseInt(selector, 10);
|
|
314
|
+
if (!Number.isNaN(n)) {
|
|
315
|
+
const byPane = members.find((m) => m.pane?.endsWith(`.${n}`) || m.pane?.endsWith(`:${n}`));
|
|
316
|
+
if (byPane) return byPane;
|
|
317
|
+
if (n >= 1 && n <= members.length) return members[n - 1];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function publishLeadControl(state, targetMember, command, reason = "") {
|
|
324
|
+
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
325
|
+
const leadAgent = (state?.members || []).find((m) => m.role === "lead")?.agentId || "lead";
|
|
326
|
+
|
|
327
|
+
const payload = {
|
|
328
|
+
from_agent: leadAgent,
|
|
329
|
+
to_agent: targetMember.agentId,
|
|
330
|
+
command,
|
|
331
|
+
reason,
|
|
332
|
+
payload: {
|
|
333
|
+
issued_by: leadAgent,
|
|
334
|
+
issued_at: Date.now(),
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const res = await fetch(`${hubBase}/bridge/control`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "Content-Type": "application/json" },
|
|
342
|
+
body: JSON.stringify(payload),
|
|
343
|
+
});
|
|
344
|
+
return !!res.ok;
|
|
345
|
+
} catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|