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.
Files changed (50) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +2 -0
  4. package/README.md +2 -0
  5. package/bin/triflux.mjs +297 -47
  6. package/hooks/hook-registry.json +4 -4
  7. package/hub/fullcycle.mjs +96 -0
  8. package/hub/paths.mjs +30 -28
  9. package/hub/pipeline/index.mjs +318 -318
  10. package/hub/schema.sql +146 -146
  11. package/hub/team/cli/commands/kill.mjs +37 -37
  12. package/hub/team/cli/commands/stop.mjs +31 -31
  13. package/hub/team/cli/commands/task.mjs +30 -30
  14. package/hub/team/cli/services/hub-client.mjs +208 -208
  15. package/hub/team/cli/services/native-control.mjs +118 -118
  16. package/hub/team/cli/services/runtime-mode.mjs +62 -62
  17. package/hub/team/cli/services/state-store.mjs +48 -48
  18. package/hub/team/dashboard.mjs +274 -274
  19. package/hub/team/native.mjs +649 -649
  20. package/hub/team/psmux.mjs +68 -13
  21. package/hub/tools.mjs +554 -554
  22. package/hub/workers/claude-worker.mjs +423 -423
  23. package/hub/workers/codex-mcp.mjs +410 -410
  24. package/hub/workers/gemini-worker.mjs +429 -429
  25. package/hub/workers/interface.mjs +40 -40
  26. package/package.json +1 -1
  27. package/scripts/__tests__/remote-spawn-transfer.test.mjs +1 -1
  28. package/scripts/cache-warmup.mjs +1 -0
  29. package/scripts/claude-logged.ps1 +54 -0
  30. package/scripts/demo-tui.mjs +59 -0
  31. package/scripts/headless-guard.mjs +4 -7
  32. package/scripts/hub-ensure.mjs +120 -120
  33. package/scripts/lib/psmux-info.mjs +119 -0
  34. package/scripts/lib/remote-spawn-transfer.mjs +1 -1
  35. package/scripts/setup.mjs +150 -6
  36. package/scripts/tfx-route-post.mjs +90 -13
  37. package/scripts/token-snapshot.mjs +575 -575
  38. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  39. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  40. package/skills/.omc/state/last-tool-error.json +7 -0
  41. package/skills/.omc/state/subagent-tracking.json +7 -0
  42. package/skills/tfx-codex-swarm/SKILL.md +40 -5
  43. package/skills/tfx-codex-swarm/mcp-daemon/register-autostart.ps1 +32 -0
  44. package/skills/tfx-doctor/SKILL.md +3 -0
  45. package/skills/tfx-fullcycle/SKILL.md +79 -4
  46. package/skills/tfx-hub/SKILL.md +3 -1
  47. package/skills/tfx-psmux-rules/SKILL.md +53 -31
  48. package/skills/tfx-remote-spawn/references/hosts.json +16 -16
  49. package/skills/tfx-setup/SKILL.md +9 -0
  50. package/tui/doctor.mjs +1 -0
@@ -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
- " winget install marlocarlo.psmux\n" +
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 [title = "", paneId = "", dead = "0", deadStatus = ""] = line.split("\t");
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
- "설치: winget install marlocarlo.psmux (또는 scoop install psmux / npm i -g psmux)"
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("psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)");
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("psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)");
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
- try {
1130
- psmuxExec(["send-keys", "-t", paneId, "exit", "Enter"]);
1131
- } catch {
1132
- // send-keys 실패 무시
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("psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)");
1215
+ throw new Error(`psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
1161
1216
  }
1162
1217
  try {
1163
1218
  const { paneId } = getWorkerStatus(sessionName, workerName);