triflux 9.7.13 → 9.8.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +2 -0
- package/README.md +2 -0
- package/bin/triflux.mjs +297 -47
- package/hooks/hook-registry.json +4 -4
- package/hub/fullcycle.mjs +96 -0
- package/hub/paths.mjs +30 -28
- package/hub/pipeline/index.mjs +318 -318
- package/hub/schema.sql +146 -146
- package/hub/team/cli/commands/kill.mjs +37 -37
- package/hub/team/cli/commands/stop.mjs +31 -31
- package/hub/team/cli/commands/task.mjs +30 -30
- package/hub/team/cli/services/hub-client.mjs +208 -208
- package/hub/team/cli/services/native-control.mjs +118 -118
- package/hub/team/cli/services/runtime-mode.mjs +62 -62
- package/hub/team/cli/services/state-store.mjs +48 -48
- package/hub/team/dashboard.mjs +274 -274
- package/hub/team/native.mjs +649 -649
- package/hub/team/psmux.mjs +68 -13
- package/hub/tools.mjs +554 -554
- package/hub/workers/claude-worker.mjs +423 -423
- package/hub/workers/codex-mcp.mjs +410 -410
- package/hub/workers/gemini-worker.mjs +429 -429
- package/hub/workers/interface.mjs +40 -40
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +1 -1
- package/scripts/cache-warmup.mjs +1 -0
- package/scripts/claude-logged.ps1 +54 -0
- package/scripts/demo-tui.mjs +59 -0
- package/scripts/headless-guard.mjs +4 -7
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +1 -1
- package/scripts/setup.mjs +150 -6
- package/scripts/tfx-route-post.mjs +90 -13
- package/scripts/token-snapshot.mjs +575 -575
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/tfx-codex-swarm/SKILL.md +40 -5
- package/skills/tfx-codex-swarm/mcp-daemon/register-autostart.ps1 +32 -0
- package/skills/tfx-doctor/SKILL.md +3 -0
- package/skills/tfx-fullcycle/SKILL.md +79 -4
- package/skills/tfx-hub/SKILL.md +3 -1
- package/skills/tfx-psmux-rules/SKILL.md +53 -31
- package/skills/tfx-remote-spawn/references/hosts.json +16 -16
- package/skills/tfx-setup/SKILL.md +9 -0
- package/tui/doctor.mjs +1 -0
package/hub/team/psmux.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import childProcess from "node:child_process";
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir, homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { formatPsmuxInstallGuidance } from "../../scripts/lib/psmux-info.mjs";
|
|
7
8
|
|
|
8
9
|
const PSMUX_BIN = (() => {
|
|
9
10
|
if (process.env.PSMUX_BIN) return process.env.PSMUX_BIN;
|
|
@@ -166,9 +167,7 @@ function ensurePsmuxInstalled() {
|
|
|
166
167
|
"psmux가 설치되어 있지 않습니다.\n\n" +
|
|
167
168
|
"psmux는 Codex/Gemini CLI를 병렬 세션으로 실행하는 터미널 멀티플렉서입니다.\n" +
|
|
168
169
|
"설치 방법 (택 1):\n" +
|
|
169
|
-
"
|
|
170
|
-
" scoop install psmux\n" +
|
|
171
|
-
" npm install -g psmux\n\n" +
|
|
170
|
+
`${formatPsmuxInstallGuidance(" ")}\n\n` +
|
|
172
171
|
"설치 후 터미널을 재시작하세요."
|
|
173
172
|
);
|
|
174
173
|
}
|
|
@@ -261,13 +260,19 @@ function parsePaneDetails(output) {
|
|
|
261
260
|
.map((line) => line.trim())
|
|
262
261
|
.filter(Boolean)
|
|
263
262
|
.map((line) => {
|
|
264
|
-
const
|
|
263
|
+
const parts = line.split("\t");
|
|
264
|
+
const hasPaneIndex = parts.length >= 5;
|
|
265
|
+
const [paneIndexText = "", title = "", paneId = "", dead = "0", deadStatus = ""] = hasPaneIndex
|
|
266
|
+
? parts
|
|
267
|
+
: ["", ...parts];
|
|
265
268
|
const exitCode = dead === "1"
|
|
266
269
|
? Number.parseInt(deadStatus, 10)
|
|
267
270
|
: null;
|
|
271
|
+
const paneIndex = Number.parseInt(paneIndexText, 10);
|
|
268
272
|
return {
|
|
269
273
|
title,
|
|
270
274
|
paneId,
|
|
275
|
+
paneIndex: Number.isFinite(paneIndex) ? paneIndex : null,
|
|
271
276
|
isDead: dead === "1",
|
|
272
277
|
exitCode: Number.isFinite(exitCode) ? exitCode : dead === "1" ? 0 : null,
|
|
273
278
|
};
|
|
@@ -292,7 +297,7 @@ function listPaneDetails(sessionName) {
|
|
|
292
297
|
"-t",
|
|
293
298
|
sessionName,
|
|
294
299
|
"-F",
|
|
295
|
-
"#{pane_title}\t#{session_name}:#{window_index}.#{pane_index}\t#{pane_dead}\t#{pane_dead_status}",
|
|
300
|
+
"#{pane_index}\t#{pane_title}\t#{session_name}:#{window_index}.#{pane_index}\t#{pane_dead}\t#{pane_dead_status}",
|
|
296
301
|
]);
|
|
297
302
|
return parsePaneDetails(output);
|
|
298
303
|
}
|
|
@@ -617,12 +622,40 @@ function killOrphanMcpProcesses(sessionName) {
|
|
|
617
622
|
}
|
|
618
623
|
}
|
|
619
624
|
|
|
625
|
+
function detachAttachedClients(sessionName, waitMs = 750) {
|
|
626
|
+
const attachedCount = getPsmuxSessionAttachedCount(sessionName);
|
|
627
|
+
if (!Number.isFinite(attachedCount) || attachedCount <= 0) return false;
|
|
628
|
+
try {
|
|
629
|
+
psmuxExec(["detach-client", "-t", sessionName], { stdio: "ignore" });
|
|
630
|
+
sleepMs(waitMs);
|
|
631
|
+
return true;
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function findFallbackPane(sessionName, excludedPaneId) {
|
|
638
|
+
try {
|
|
639
|
+
const panes = listPaneDetails(sessionName)
|
|
640
|
+
.filter((pane) => pane.paneId !== excludedPaneId && !pane.isDead);
|
|
641
|
+
if (panes.length === 0) return null;
|
|
642
|
+
return panes.find((pane) => pane.title === "lead")
|
|
643
|
+
|| panes.find((pane) => pane.paneIndex === 0)
|
|
644
|
+
|| panes[0];
|
|
645
|
+
} catch {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
620
650
|
/**
|
|
621
651
|
* psmux 세션 종료
|
|
622
652
|
* 순서: pipe-pane 해제 → pane 프로세스 트리 정리 → 세션 종료 → 고아 정리
|
|
623
653
|
* @param {string} sessionName
|
|
624
654
|
*/
|
|
625
655
|
export function killPsmuxSession(sessionName) {
|
|
656
|
+
// attach된 WT/ConPTY 클라이언트가 있으면 먼저 안전하게 분리한다.
|
|
657
|
+
detachAttachedClients(sessionName);
|
|
658
|
+
|
|
626
659
|
// 1. pipe-pane 캡처 해제 — reader 프로세스에 EOF 전송하여 정상 종료 유도
|
|
627
660
|
let paneIds = [];
|
|
628
661
|
try {
|
|
@@ -1025,7 +1058,7 @@ export function spawnWorker(sessionName, workerName, cmd) {
|
|
|
1025
1058
|
if (!hasPsmux()) {
|
|
1026
1059
|
throw new Error(
|
|
1027
1060
|
"psmux가 설치되어 있지 않습니다.\n" +
|
|
1028
|
-
"
|
|
1061
|
+
`설치 방법:\n${formatPsmuxInstallGuidance(" ")}`
|
|
1029
1062
|
);
|
|
1030
1063
|
}
|
|
1031
1064
|
|
|
@@ -1067,7 +1100,7 @@ export function spawnWorker(sessionName, workerName, cmd) {
|
|
|
1067
1100
|
*/
|
|
1068
1101
|
export function getWorkerStatus(sessionName, workerName) {
|
|
1069
1102
|
if (!hasPsmux()) {
|
|
1070
|
-
throw new Error(
|
|
1103
|
+
throw new Error(`psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
|
|
1071
1104
|
}
|
|
1072
1105
|
try {
|
|
1073
1106
|
const pane = resolvePane(sessionName, workerName);
|
|
@@ -1092,10 +1125,22 @@ export function getWorkerStatus(sessionName, workerName) {
|
|
|
1092
1125
|
*/
|
|
1093
1126
|
export function killWorker(sessionName, workerName) {
|
|
1094
1127
|
if (!hasPsmux()) {
|
|
1095
|
-
throw new Error(
|
|
1128
|
+
throw new Error(`psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
|
|
1096
1129
|
}
|
|
1097
1130
|
try {
|
|
1098
1131
|
const { paneId, status } = getWorkerStatus(sessionName, workerName);
|
|
1132
|
+
const attachedCount = getPsmuxSessionAttachedCount(sessionName);
|
|
1133
|
+
const fallbackPane = attachedCount > 0
|
|
1134
|
+
? findFallbackPane(sessionName, paneId)
|
|
1135
|
+
: null;
|
|
1136
|
+
|
|
1137
|
+
if (fallbackPane?.paneId) {
|
|
1138
|
+
try {
|
|
1139
|
+
psmuxExec(["select-pane", "-t", fallbackPane.paneId]);
|
|
1140
|
+
} catch {
|
|
1141
|
+
// focus 회복 best-effort
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1099
1144
|
|
|
1100
1145
|
// pipe-pane 캡처 해제 — reader 프로세스 정상 종료 유도
|
|
1101
1146
|
disablePipeCapture(paneId);
|
|
@@ -1126,10 +1171,12 @@ export function killWorker(sessionName, workerName) {
|
|
|
1126
1171
|
// send-keys 실패 무시
|
|
1127
1172
|
}
|
|
1128
1173
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1174
|
+
if (!fallbackPane) {
|
|
1175
|
+
try {
|
|
1176
|
+
psmuxExec(["send-keys", "-t", paneId, "exit", "Enter"]);
|
|
1177
|
+
} catch {
|
|
1178
|
+
// send-keys 실패 무시
|
|
1179
|
+
}
|
|
1133
1180
|
}
|
|
1134
1181
|
|
|
1135
1182
|
sleepMs(2000);
|
|
@@ -1139,6 +1186,14 @@ export function killWorker(sessionName, workerName) {
|
|
|
1139
1186
|
} catch {
|
|
1140
1187
|
// 이미 종료된 pane — 무시
|
|
1141
1188
|
}
|
|
1189
|
+
|
|
1190
|
+
if (fallbackPane?.paneId) {
|
|
1191
|
+
try {
|
|
1192
|
+
psmuxExec(["select-pane", "-t", fallbackPane.paneId]);
|
|
1193
|
+
} catch {
|
|
1194
|
+
// pane 정리 후 focus 재선택 best-effort
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1142
1197
|
return { killed: true };
|
|
1143
1198
|
} catch (err) {
|
|
1144
1199
|
if (err.message.includes("워커를 찾을 수 없습니다")) {
|
|
@@ -1157,7 +1212,7 @@ export function killWorker(sessionName, workerName) {
|
|
|
1157
1212
|
*/
|
|
1158
1213
|
export function captureWorkerOutput(sessionName, workerName, lines = 50) {
|
|
1159
1214
|
if (!hasPsmux()) {
|
|
1160
|
-
throw new Error(
|
|
1215
|
+
throw new Error(`psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
|
|
1161
1216
|
}
|
|
1162
1217
|
try {
|
|
1163
1218
|
const { paneId } = getWorkerStatus(sessionName, workerName);
|