triflux 10.9.29 → 10.9.31

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 (145) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/bin/triflux.mjs +246 -17
  4. package/config/mcp-registry.json +29 -0
  5. package/hooks/error-context.mjs +1 -1
  6. package/hooks/keyword-rules.json +7 -24
  7. package/hooks/safety-guard.mjs +42 -4
  8. package/hooks/session-start-fast.mjs +54 -12
  9. package/hub/account-broker.mjs +405 -28
  10. package/hub/bridge.mjs +8 -2
  11. package/hub/cli-adapter-base.mjs +29 -6
  12. package/hub/codex-adapter.mjs +3 -1
  13. package/hub/lib/env-detect.mjs +43 -10
  14. package/hub/pipe.mjs +4 -1
  15. package/hub/platform.mjs +8 -0
  16. package/hub/server.mjs +542 -217
  17. package/hub/team/backend.mjs +5 -1
  18. package/hub/team/conductor.mjs +83 -4
  19. package/hub/team/dashboard-open.mjs +57 -13
  20. package/hub/team/git-preflight.mjs +9 -9
  21. package/hub/team/handoff.mjs +2 -1
  22. package/hub/team/headless.mjs +36 -21
  23. package/hub/team/launcher-template.mjs +2 -0
  24. package/hub/team/notify.mjs +21 -2
  25. package/hub/team/process-cleanup.mjs +2 -2
  26. package/hub/team/psmux.mjs +494 -63
  27. package/hub/team/runtime-strategy.mjs +70 -0
  28. package/hub/team/swarm-cli.mjs +186 -0
  29. package/hub/team/swarm-hypervisor.mjs +76 -16
  30. package/hub/team/swarm-planner.mjs +3 -1
  31. package/hub/team/synapse-cli.mjs +19 -5
  32. package/hub/team/synapse-registry.mjs +29 -8
  33. package/hub/team/tui-core.mjs +8 -12
  34. package/hub/team/tui-lite.mjs +2 -2
  35. package/hub/team/tui-synapse.mjs +20 -10
  36. package/hub/team/tui.mjs +35 -20
  37. package/hub/team/worktree-lifecycle.mjs +105 -36
  38. package/hub/team/wt-manager.mjs +10 -2
  39. package/hub/workers/codex-app-server-worker.mjs +1018 -0
  40. package/hub/workers/delegator-mcp.mjs +24 -12
  41. package/hub/workers/factory.mjs +78 -1
  42. package/hub/workers/gemini-worker.mjs +8 -1
  43. package/hub/workers/interface.mjs +17 -1
  44. package/hub/workers/lib/jsonrpc-stdio.mjs +464 -0
  45. package/package.json +62 -21
  46. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +7 -3
  47. package/scripts/claude-login-detect.mjs +24 -5
  48. package/scripts/config-audit.mjs +92 -22
  49. package/scripts/doctor-diagnose.mjs +64 -22
  50. package/scripts/headless-guard.mjs +6 -2
  51. package/scripts/hub-watchdog.mjs +86 -0
  52. package/scripts/keyword-detector.mjs +3 -1
  53. package/scripts/lib/cross-review-utils.mjs +6 -1
  54. package/scripts/lib/keyword-rules.mjs +1 -3
  55. package/scripts/lib/mcp-filter.mjs +2 -1
  56. package/scripts/preinstall.mjs +4 -1
  57. package/scripts/release/lib.mjs +10 -4
  58. package/scripts/remote-spawn.mjs +13 -5
  59. package/scripts/session-stale-cleanup.mjs +19 -10
  60. package/scripts/setup.mjs +17 -13
  61. package/scripts/sync-codex-auth.mjs +71 -0
  62. package/scripts/sync-hub-mcp-settings.mjs +185 -0
  63. package/scripts/tfx-route.sh +15 -3
  64. package/skills/tfx-auto/SKILL.md +22 -26
  65. package/skills/tfx-deep-analysis/SKILL.md +6 -1
  66. package/skills/tfx-doctor/SKILL.md +0 -21
  67. package/skills/tfx-hub/SKILL.md +85 -273
  68. package/skills/tfx-panel/SKILL.md +1 -1
  69. package/skills/tfx-panel/SKILL.md.tmpl +1 -1
  70. package/skills/tfx-prune/SKILL.md +1 -1
  71. package/skills/tfx-prune/SKILL.md.tmpl +1 -1
  72. package/skills/tfx-setup/SKILL.md +41 -0
  73. package/skills/tfx-setup/SKILL.md.tmpl +41 -0
  74. package/tui/codex-profile.mjs +457 -0
  75. package/tui/core.mjs +266 -0
  76. package/tui/doctor.mjs +375 -0
  77. package/tui/gemini-profile.mjs +299 -0
  78. package/tui/monitor-data.mjs +152 -0
  79. package/tui/monitor.mjs +330 -0
  80. package/tui/setup.mjs +598 -0
  81. package/CLAUDE.md +0 -212
  82. package/references/hosts.json +0 -46
  83. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  84. package/skills/tfx-workspace/evals/evals.json +0 -79
  85. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  86. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  87. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  88. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  89. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  90. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  91. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  92. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  93. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  94. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  95. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  96. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  97. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  98. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  99. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  100. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  101. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  102. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  103. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  104. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  105. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  106. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  107. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  108. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  109. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  110. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  111. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  112. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  113. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  114. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  115. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  116. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  117. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  118. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  119. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  120. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  121. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  122. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  123. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  124. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  125. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  126. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  127. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  128. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  129. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  130. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  131. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  132. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  133. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  134. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  135. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  136. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  137. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  138. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  139. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  140. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  141. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  142. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  143. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  144. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  145. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "triflux",
4
+ "description": "CLI-first multi-model orchestrator — Codex/Gemini/Claude routing with DAG execution, auto-triage, and cost optimization",
5
+ "owner": {
6
+ "name": "tellang"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "triflux",
11
+ "description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
12
+ "version": "10.9.31",
13
+ "author": {
14
+ "name": "tellang"
15
+ },
16
+ "source": {
17
+ "source": "npm",
18
+ "package": "triflux"
19
+ },
20
+ "category": "productivity",
21
+ "homepage": "https://github.com/tellang/triflux",
22
+ "tags": [
23
+ "multi-model",
24
+ "codex",
25
+ "gemini",
26
+ "cli-routing",
27
+ "orchestration",
28
+ "cost-optimization",
29
+ "dag-execution"
30
+ ]
31
+ }
32
+ ],
33
+ "version": "10.9.31"
34
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "triflux",
3
+ "version": "10.9.31",
4
+ "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
+ "author": {
6
+ "name": "tellang"
7
+ },
8
+ "repository": "https://github.com/tellang/triflux",
9
+ "homepage": "https://github.com/tellang/triflux",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "claude-code",
13
+ "plugin",
14
+ "codex",
15
+ "gemini",
16
+ "cli-routing",
17
+ "orchestration",
18
+ "multi-model"
19
+ ],
20
+ "skills": "./skills/",
21
+ "hooks": "./hooks/hooks.json"
22
+ }
package/bin/triflux.mjs CHANGED
@@ -154,7 +154,8 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
154
154
  {
155
155
  name: "--diagnose",
156
156
  type: "boolean",
157
- description: "진단 번들(zip) 생성: spawn-trace + hook timing + system info",
157
+ description:
158
+ "진단 번들(zip) 생성: spawn-trace + hook timing + system info",
158
159
  },
159
160
  {
160
161
  name: "--json",
@@ -308,6 +309,42 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
308
309
  },
309
310
  ],
310
311
  },
312
+ swarm: {
313
+ usage: "tfx swarm <prd-path> [--dry-run|--json|--filter <shard>]",
314
+ description: "PRD 기반 멀티모델 x 멀티기기 스웜 실행 (#93)",
315
+ subcommands: {
316
+ plan: "tfx swarm plan <prd-path> [--json] — 실행 없이 계획만 출력",
317
+ list: "tfx swarm list [--json] — 활성 스웜 세션 조회 (synapse status)",
318
+ status: "tfx swarm status [--json] — list alias",
319
+ },
320
+ options: [
321
+ {
322
+ name: "--dry-run",
323
+ type: "boolean",
324
+ description: "PRD 분석만 수행, shard를 실행하지 않음",
325
+ },
326
+ {
327
+ name: "--json",
328
+ type: "boolean",
329
+ description: "구조화된 JSON 출력",
330
+ },
331
+ {
332
+ name: "--filter",
333
+ type: "string",
334
+ description: "특정 shard만 실행 (이름 매칭)",
335
+ },
336
+ {
337
+ name: "--max-restarts",
338
+ type: "number",
339
+ description: "shard별 최대 재시작 횟수 (기본 2)",
340
+ },
341
+ {
342
+ name: "--logs-dir",
343
+ type: "string",
344
+ description: "이벤트 로그 출력 디렉토리 오버라이드",
345
+ },
346
+ ],
347
+ },
311
348
  why: {
312
349
  usage: "tfx why <path> [--json]",
313
350
  description: "해당 경로의 마지막 커밋에서 X-Intent 트레일러 추출",
@@ -525,6 +562,10 @@ function whichInShell(cmd, shell) {
525
562
  ["-c", `source ~/.bashrc 2>/dev/null && command -v "${cmd}" 2>/dev/null`],
526
563
  ],
527
564
  cmd: ["cmd", ["/c", "where", cmd]],
565
+ zsh: [
566
+ "zsh",
567
+ ["-c", `source ~/.zshrc 2>/dev/null && command -v "${cmd}" 2>/dev/null`],
568
+ ],
528
569
  pwsh: [
529
570
  "pwsh",
530
571
  [
@@ -852,7 +893,7 @@ function getClaudeRoutingSyncSummary(results) {
852
893
 
853
894
  function checkCliCrossShell(cmd, installHint) {
854
895
  const shells =
855
- process.platform === "win32" ? ["bash", "cmd", "pwsh"] : ["bash"];
896
+ process.platform === "win32" ? ["bash", "cmd", "pwsh"] : ["bash", "zsh"];
856
897
  let anyFound = false;
857
898
  let bashMissing = false;
858
899
  const shellResults = [];
@@ -1186,6 +1227,24 @@ function cmdSetup(options = {}) {
1186
1227
  }
1187
1228
  }
1188
1229
 
1230
+ // ── tmux 기본 셸 확인 (macOS/Linux) ──
1231
+ if (process.platform !== "win32" && which("tmux")) {
1232
+ try {
1233
+ const shellOut = execSync(
1234
+ "tmux show-options -g default-shell 2>/dev/null",
1235
+ {
1236
+ encoding: "utf8",
1237
+ timeout: 3000,
1238
+ },
1239
+ ).trim();
1240
+ if (shellOut) {
1241
+ ok(`tmux 기본 셸: ${shellOut.split(/\s+/).pop() || "확인 완료"}`);
1242
+ }
1243
+ } catch {
1244
+ /* tmux 서버 미실행 — 무시 */
1245
+ }
1246
+ }
1247
+
1189
1248
  // ── 결과 추적 ──
1190
1249
  const summary = [];
1191
1250
 
@@ -1881,7 +1940,10 @@ async function cmdDoctor(options = {}) {
1881
1940
  }
1882
1941
  {
1883
1942
  const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
1884
- if (claudeGuide.skipped && claudeGuide.reason !== "global_sync_disabled")
1943
+ if (
1944
+ claudeGuide.skipped &&
1945
+ claudeGuide.reason !== "global_sync_disabled"
1946
+ )
1885
1947
  warn(`CLAUDE.md 라우팅 섹션 확인 실패: ${claudeGuide.reason}`);
1886
1948
  else if (
1887
1949
  claudeGuide.action === "created" ||
@@ -2449,6 +2511,86 @@ async function cmdDoctor(options = {}) {
2449
2511
  addDoctorCheck(report, { name: "stale-skills", status: "ok" });
2450
2512
  }
2451
2513
 
2514
+ // 8.5 Dev 의존성 (npm link 환경에서 node_modules 누락 감지 — Issue #101)
2515
+ section("Dev Dependencies");
2516
+ try {
2517
+ const pkgJsonPath = join(PKG_ROOT, "package.json");
2518
+ const nodeModulesPath = join(PKG_ROOT, "node_modules");
2519
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
2520
+ const runtimeDeps = Object.keys(pkgJson.dependencies || {});
2521
+ const isLinkedDev = (() => {
2522
+ try {
2523
+ const stat = statSync(nodeModulesPath);
2524
+ return stat.isDirectory();
2525
+ } catch {
2526
+ return false;
2527
+ }
2528
+ })();
2529
+ const missingDeps = [];
2530
+ if (!isLinkedDev) {
2531
+ // node_modules 전체 누락 — 전역 install 또는 미압축 릴리즈일 가능성
2532
+ if (runtimeDeps.length > 0) {
2533
+ missingDeps.push(...runtimeDeps);
2534
+ }
2535
+ } else {
2536
+ for (const dep of runtimeDeps) {
2537
+ const depPath = join(nodeModulesPath, ...dep.split("/"));
2538
+ if (!existsSync(depPath)) missingDeps.push(dep);
2539
+ }
2540
+ }
2541
+ if (missingDeps.length === 0) {
2542
+ ok(
2543
+ `의존성 ${runtimeDeps.length}개 설치됨${
2544
+ isLinkedDev ? ` ${DIM}(dev)${RESET}` : ""
2545
+ }`,
2546
+ );
2547
+ addDoctorCheck(report, {
2548
+ name: "dev-deps",
2549
+ status: "ok",
2550
+ total: runtimeDeps.length,
2551
+ linkedDev: isLinkedDev,
2552
+ });
2553
+ } else {
2554
+ const head = missingDeps.slice(0, 5);
2555
+ const tail = missingDeps.length > 5 ? `+${missingDeps.length - 5}` : "";
2556
+ warn(
2557
+ `누락 의존성 ${missingDeps.length}개: ${head.join(", ")}${tail ? ` ${tail}` : ""}`,
2558
+ );
2559
+ info("수정: tfx doctor --fix 또는 `npm install` (PKG_ROOT 내)");
2560
+ addDoctorCheck(report, {
2561
+ name: "dev-deps",
2562
+ status: "missing",
2563
+ missing: missingDeps,
2564
+ linkedDev: isLinkedDev,
2565
+ pkgRoot: PKG_ROOT,
2566
+ fix: "tfx doctor --fix",
2567
+ });
2568
+ issues++;
2569
+ if (fix) {
2570
+ // --fix 모드: npm install 실행 (Windows 호환 shell: true)
2571
+ info(`npm install 실행 중 (${PKG_ROOT})...`);
2572
+ try {
2573
+ const { execFileSync } = await import("node:child_process");
2574
+ execFileSync("npm", ["install", "--no-audit", "--no-fund"], {
2575
+ cwd: PKG_ROOT,
2576
+ stdio: "inherit",
2577
+ shell: process.platform === "win32",
2578
+ });
2579
+ ok("npm install 완료 — 의존성 복구됨");
2580
+ } catch (err) {
2581
+ warn(`npm install 실패: ${err?.message || err}`);
2582
+ }
2583
+ }
2584
+ }
2585
+ } catch (err) {
2586
+ warn(`dev 의존성 체크 실패: ${err?.message || err}`);
2587
+ addDoctorCheck(report, {
2588
+ name: "dev-deps",
2589
+ status: "error",
2590
+ error: String(err?.message || err),
2591
+ });
2592
+ }
2593
+
2452
2594
  // 9. 플러그인 등록
2453
2595
  section("Plugin");
2454
2596
  const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
@@ -4794,13 +4936,35 @@ async function cmdHub(args = [], options = {}) {
4794
4936
  });
4795
4937
  }
4796
4938
 
4939
+ // Issue #102: spawn stderr 를 임시 파일로 캡처해 실패 시 root cause 노출.
4940
+ // detached spawn 은 pipe 유지가 까다로우니 fd 리다이렉트로 접근.
4941
+ const { openSync: _openSync, closeSync: _closeSync } = await import(
4942
+ "node:fs"
4943
+ );
4944
+ const { tmpdir: _tmpdir } = await import("node:os");
4945
+ const startupErrPath = join(
4946
+ _tmpdir(),
4947
+ `tfx-hub-start-${Date.now()}-${process.pid}.err`,
4948
+ );
4949
+ let errFd;
4950
+ try {
4951
+ errFd = _openSync(startupErrPath, "w");
4952
+ } catch {
4953
+ errFd = undefined;
4954
+ }
4955
+
4797
4956
  const child = spawn(process.execPath, [serverPath], {
4798
4957
  env: { ...process.env, TFX_HUB_PORT: port },
4799
- stdio: "ignore",
4958
+ stdio: ["ignore", "ignore", errFd ?? "ignore"],
4800
4959
  detached: true,
4801
4960
  windowsHide: true,
4802
4961
  });
4803
4962
  child.unref();
4963
+ if (errFd !== undefined) {
4964
+ try {
4965
+ _closeSync(errFd);
4966
+ } catch {}
4967
+ }
4804
4968
 
4805
4969
  // PID 파일 확인 (최대 3초 대기, 100ms 폴링)
4806
4970
  let started = false;
@@ -4824,13 +4988,46 @@ async function cmdHub(args = [], options = {}) {
4824
4988
  console.log("");
4825
4989
  autoRegisterMcp(hubInfo.url, { codexEnabled: true });
4826
4990
  console.log("");
4991
+ // 성공했으면 임시 stderr 파일 정리
4992
+ try {
4993
+ unlinkSync(startupErrPath);
4994
+ } catch {}
4827
4995
  } else {
4828
- // 직접 포그라운드 모드로 안내
4829
- console.log(
4830
- `\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패 — 포그라운드로 실행:`,
4831
- );
4996
+ // Issue #102: 캡처된 stderr 에서 root cause 추출
4997
+ let rootCause = "";
4998
+ try {
4999
+ rootCause = readFileSync(startupErrPath, "utf8").trim();
5000
+ } catch {}
5001
+
5002
+ console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패`);
5003
+
5004
+ if (rootCause) {
5005
+ // 가장 유용한 에러 라인 강조 (ERR_*, Error:, throw)
5006
+ const highlight = rootCause
5007
+ .split(/\r?\n/)
5008
+ .find((line) => /ERR_[A-Z_]+|^Error:|cannot find/i.test(line));
5009
+ if (highlight) {
5010
+ console.log(` ${RED}▸ ${highlight.trim()}${RESET}`);
5011
+ }
5012
+ console.log(`\n ${DIM}전체 로그: ${startupErrPath}${RESET}`);
5013
+ // 원인별 실전 힌트
5014
+ if (/Cannot find package/i.test(rootCause)) {
5015
+ console.log(
5016
+ ` ${DIM}힌트: \`cd ${PKG_ROOT} && npm install\` 로 의존성 복구 (특히 \`npm link\` 환경).${RESET}`,
5017
+ );
5018
+ } else if (/EADDRINUSE/i.test(rootCause)) {
5019
+ console.log(
5020
+ ` ${DIM}힌트: 포트 ${port} 이 이미 사용 중. \`tfx hub stop\` 후 재시도.${RESET}`,
5021
+ );
5022
+ }
5023
+ } else {
5024
+ console.log(
5025
+ ` ${DIM}stderr 캡처 실패 — 아래 명령으로 포그라운드 실행해 원인 확인:${RESET}`,
5026
+ );
5027
+ }
5028
+
4832
5029
  console.log(
4833
- ` ${DIM}TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`,
5030
+ `\n ${DIM}포그라운드 실행: TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`,
4834
5031
  );
4835
5032
  }
4836
5033
  break;
@@ -5198,9 +5395,15 @@ async function main() {
5198
5395
  const auditScript = join(PKG_ROOT, "scripts", "config-audit.mjs");
5199
5396
  const auditArgs = JSON_OUTPUT ? ["--json"] : [];
5200
5397
  try {
5201
- const out = execFileSync(process.execPath, [auditScript, ...auditArgs], {
5202
- timeout: 15000, encoding: "utf8", windowsHide: true,
5203
- });
5398
+ const out = execFileSync(
5399
+ process.execPath,
5400
+ [auditScript, ...auditArgs],
5401
+ {
5402
+ timeout: 15000,
5403
+ encoding: "utf8",
5404
+ windowsHide: true,
5405
+ },
5406
+ );
5204
5407
  process.stdout.write(out);
5205
5408
  } catch (e) {
5206
5409
  process.stdout.write(e.stdout || "");
@@ -5213,8 +5416,12 @@ async function main() {
5213
5416
  const result = await diagnose({ json: JSON_OUTPUT });
5214
5417
  if (!JSON_OUTPUT) {
5215
5418
  if (result.ok) {
5216
- console.log(`\n ${GREEN_BRIGHT}✓${RESET} 진단 번들 생성: ${result.zipPath}`);
5217
- console.log(` spawn 이벤트: ${result.traceCount}건, 타이밍: ${result.hookTimingCount}건\n`);
5419
+ console.log(
5420
+ `\n ${GREEN_BRIGHT}✓${RESET} 진단 번들 생성: ${result.zipPath}`,
5421
+ );
5422
+ console.log(
5423
+ ` spawn 이벤트: ${result.traceCount}건, 훅 타이밍: ${result.hookTimingCount}건\n`,
5424
+ );
5218
5425
  } else {
5219
5426
  console.log(`\n ${RED}✗${RESET} 진단 실패: ${result.error}\n`);
5220
5427
  }
@@ -5342,9 +5549,7 @@ async function main() {
5342
5549
  return;
5343
5550
  }
5344
5551
  case "synapse": {
5345
- const { cmdSynapseStatus } = await import(
5346
- "../hub/team/synapse-cli.mjs"
5347
- );
5552
+ const { cmdSynapseStatus } = await import("../hub/team/synapse-cli.mjs");
5348
5553
  const sub = cmdArgs[0] || "status";
5349
5554
  if (sub !== "status") {
5350
5555
  throw createCliError(`synapse 서브커맨드 미지원: ${sub}`, {
@@ -5356,6 +5561,30 @@ async function main() {
5356
5561
  await cmdSynapseStatus(cmdArgs.slice(1), { json: JSON_OUTPUT });
5357
5562
  return;
5358
5563
  }
5564
+ case "swarm": {
5565
+ await checkHubRunning();
5566
+ const { cmdSwarmRun, cmdSwarmPlan, cmdSwarmList } = await import(
5567
+ "../hub/team/swarm-cli.mjs"
5568
+ );
5569
+ const sub = cmdArgs[0] || "";
5570
+ if (sub === "list" || sub === "status") {
5571
+ await cmdSwarmList(cmdArgs.slice(1), { json: JSON_OUTPUT });
5572
+ return;
5573
+ }
5574
+ if (sub === "plan") {
5575
+ await cmdSwarmPlan(cmdArgs.slice(1), { json: JSON_OUTPUT });
5576
+ return;
5577
+ }
5578
+ if (!sub || sub.startsWith("--")) {
5579
+ throw createCliError("PRD 경로가 필요합니다", {
5580
+ exitCode: EXIT_ARG_ERROR,
5581
+ reason: "argError",
5582
+ fix: "tfx swarm <prd-path> [--dry-run|--json|--filter <shard>]",
5583
+ });
5584
+ }
5585
+ await cmdSwarmRun(cmdArgs, { json: JSON_OUTPUT });
5586
+ return;
5587
+ }
5359
5588
  case "why": {
5360
5589
  const { cmdSynapseWhy } = await import("../hub/team/synapse-cli.mjs");
5361
5590
  await cmdSynapseWhy(cmdArgs, { json: JSON_OUTPUT });
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "mcp-registry-schema",
3
+ "version": 1,
4
+ "description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
5
+ "defaults": {
6
+ "transport": "hub-url",
7
+ "hub_base": "http://127.0.0.1:27888"
8
+ },
9
+ "servers": {
10
+ "tfx-hub": {
11
+ "transport": "hub-url",
12
+ "url": "http://127.0.0.1:27888/mcp",
13
+ "safe": true,
14
+ "targets": ["claude", "gemini", "codex"],
15
+ "description": "triflux Hub MCP 서버"
16
+ }
17
+ },
18
+ "policies": {
19
+ "stdio_action": "replace-with-hub",
20
+ "unknown_server_action": "warn",
21
+ "watched_paths": [
22
+ "~/.gemini/settings.json",
23
+ "~/.codex/config.toml",
24
+ "~/.claude/settings.json",
25
+ "~/.claude/settings.local.json",
26
+ ".mcp.json"
27
+ ]
28
+ }
29
+ }
@@ -16,7 +16,7 @@ const ERROR_HINTS = [
16
16
  },
17
17
  {
18
18
  pattern: /EACCES.*permission denied/i,
19
- hint: "권한 부족. Windows에서는 관리자 권한, Unix에서는 chmod/sudo를 확인하세요.",
19
+ hint: "권한 부족. macOS/Linux에서는 chmod/sudo, Windows에서는 관리자 권한을 확인하세요.",
20
20
  },
21
21
  {
22
22
  pattern: /EADDRINUSE/i,
@@ -11,12 +11,7 @@
11
11
  "skill": null,
12
12
  "action": "suppress_all",
13
13
  "priority": 0,
14
- "supersedes": [
15
- "tfx-multi",
16
- "tfx-unified",
17
- "tfx-codex",
18
- "tfx-gemini"
19
- ],
14
+ "supersedes": ["tfx-multi", "tfx-unified", "tfx-codex", "tfx-gemini"],
20
15
  "exclusive": true,
21
16
  "state": null,
22
17
  "mcp_route": null
@@ -62,9 +57,7 @@
62
57
  ],
63
58
  "skill": "tfx-swarm",
64
59
  "priority": 1,
65
- "supersedes": [
66
- "tfx-codex"
67
- ],
60
+ "supersedes": ["tfx-codex"],
68
61
  "exclusive": false,
69
62
  "state": null,
70
63
  "mcp_route": null
@@ -111,9 +104,7 @@
111
104
  ],
112
105
  "skill": "tfx-auto",
113
106
  "priority": 2,
114
- "supersedes": [
115
- "tfx-auto-codex"
116
- ],
107
+ "supersedes": ["tfx-auto-codex"],
117
108
  "exclusive": false,
118
109
  "state": null,
119
110
  "mcp_route": null
@@ -249,9 +240,7 @@
249
240
  ],
250
241
  "skill": "tfx-wt",
251
242
  "priority": 1,
252
- "supersedes": [
253
- "tfx-unified"
254
- ],
243
+ "supersedes": ["tfx-unified"],
255
244
  "exclusive": false,
256
245
  "state": null,
257
246
  "mcp_route": null
@@ -270,9 +259,7 @@
270
259
  ],
271
260
  "skill": "tfx-wt",
272
261
  "priority": 1,
273
- "supersedes": [
274
- "tfx-unified"
275
- ],
262
+ "supersedes": ["tfx-unified"],
276
263
  "exclusive": false,
277
264
  "state": null,
278
265
  "mcp_route": null
@@ -291,9 +278,7 @@
291
278
  ],
292
279
  "skill": "tfx-wt",
293
280
  "priority": 1,
294
- "supersedes": [
295
- "tfx-unified"
296
- ],
281
+ "supersedes": ["tfx-unified"],
297
282
  "exclusive": false,
298
283
  "state": null,
299
284
  "mcp_route": null
@@ -312,9 +297,7 @@
312
297
  ],
313
298
  "skill": "tfx-wt",
314
299
  "priority": 1,
315
- "supersedes": [
316
- "tfx-unified"
317
- ],
300
+ "supersedes": ["tfx-unified"],
318
301
  "exclusive": false,
319
302
  "state": null,
320
303
  "mcp_route": null
@@ -11,6 +11,22 @@
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
 
14
+ // ── 로컬 우회 플래그 ──────────────────────────────────────────
15
+ // LOCAL ONLY — Issue #89 대안 API 구현 전까지 psmux kill 시리즈 우회용.
16
+ // 활성 조건 (OR):
17
+ // 1) env: TFX_CLEANUP_BYPASS=1
18
+ // 2) 파일: .claude/cleanup-bypass 존재 (repo root)
19
+ // 파일 방식은 Claude Code Bash 도구가 훅에 env 전달 못하는 경우에도 동작.
20
+ // 정식 해결: hub/team/psmux.mjs 에 listSessions/killSessionByTitle/pruneStale 노출.
21
+ const CLEANUP_BYPASS = (() => {
22
+ if (process.env.TFX_CLEANUP_BYPASS === "1") return true;
23
+ try {
24
+ return existsSync(join(process.cwd(), ".claude", "cleanup-bypass"));
25
+ } catch {
26
+ return false;
27
+ }
28
+ })();
29
+
14
30
  // ── 차단 규칙 ──────────────────────────────────────────────
15
31
  const BLOCK_RULES = [
16
32
  {
@@ -44,14 +60,16 @@ const BLOCK_RULES = [
44
60
  {
45
61
  pattern: /\bpsmux\s+kill-session\b/i,
46
62
  reason:
47
- "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>",
63
+ "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 대안: listSessions()/killSessionByTitle()/pruneStale() 또는 node hub/team/psmux.mjs --internal kill-by-title <prefix|/regex/> (또는 TFX_CLEANUP_BYPASS=1/.claude/cleanup-bypass)",
48
64
  skipIfGit: true,
65
+ cleanupBypass: true,
49
66
  },
50
67
  {
51
68
  pattern: /\bpsmux\s+kill-server\b/i,
52
69
  reason:
53
- "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용",
70
+ "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용 (또는 TFX_CLEANUP_BYPASS=1)",
54
71
  skipIfGit: true,
72
+ cleanupBypass: true,
55
73
  },
56
74
  ];
57
75
 
@@ -70,8 +88,11 @@ const WT_DIRECT_BLOCK_MESSAGE =
70
88
  " wt.createTab({ title, command, profile, cwd }) — 새 탭\n" +
71
89
  " wt.splitPane({ direction: 'H'|'V', title, command }) — 패인 분할\n" +
72
90
  " wt.applySplitLayout([{ title, command, direction }]) — 다중 배치\n" +
73
- '사용법: node -e "import(\'./hub/team/wt-manager.mjs\').then(m => { const wt = m.createWtManager(); wt.createTab({ title: \'제목\', command: \'pwsh\' }); })"';
91
+ "사용법: node -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.createTab({ title: '제목', command: 'pwsh' }); })\"";
74
92
 
93
+ const PSMUX_INTERNAL_WRAPPER_PATTERNS = [
94
+ /node(?:\.exe)?\s+.*hub[\\/]+team[\\/]+psmux\.mjs\s+--internal\s+(?:list|kill-by-title|prune-stale)\b/i,
95
+ ];
75
96
 
76
97
  // ── SSH+PowerShell bash 문법 차단 ────────────────────────────
77
98
  // 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
@@ -261,6 +282,10 @@ function main() {
261
282
  return hasSegmentInvocation(cmd, [/\bpsmux\s+kill-(session|server)\b/i]);
262
283
  }
263
284
 
285
+ function isAllowedPsmuxWrapperInvocation(cmd) {
286
+ return hasSegmentInvocation(cmd, PSMUX_INTERNAL_WRAPPER_PATTERNS);
287
+ }
288
+
264
289
  function isWtDirectInvocation(cmd) {
265
290
  return hasSegmentInvocation(cmd, WT_DIRECT_PATTERNS);
266
291
  }
@@ -269,6 +294,10 @@ function main() {
269
294
  blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
270
295
  }
271
296
 
297
+ if (isAllowedPsmuxWrapperInvocation(command)) {
298
+ process.exit(0);
299
+ }
300
+
272
301
  // 0.1. reflexion 적응형 패널티 — 이전 세션에서 차단된 패턴 사전 경고
273
302
  const penalties = loadReflexionPenalties();
274
303
  if (penalties.length > 0) {
@@ -299,7 +328,10 @@ function main() {
299
328
 
300
329
  // 0.5. SSH → Windows(PowerShell) 호스트에만 bash 문법 전달 차단
301
330
  // macOS/Linux 대상은 bash/zsh이므로 허용. hosts.json OS로 판별.
302
- if (hasSegmentInvocation(command, [/^\s*ssh\s+/i]) && isSshTargetWindows(command)) {
331
+ if (
332
+ hasSegmentInvocation(command, [/^\s*ssh\s+/i]) &&
333
+ isSshTargetWindows(command)
334
+ ) {
303
335
  const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
304
336
  for (const seg of segments) {
305
337
  const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
@@ -317,12 +349,18 @@ function main() {
317
349
 
318
350
  // 1. BLOCK 체크 — exit 2로 차단
319
351
  for (const rule of BLOCK_RULES) {
352
+ if (rule.cleanupBypass && CLEANUP_BYPASS) continue;
320
353
  if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
321
354
  if (rule.pattern.test(command)) {
322
355
  blockCommand(`[triflux safety-guard] BLOCKED: ${rule.reason}`, command);
323
356
  }
324
357
  }
325
358
 
359
+ // wt 정리 명령 우회 (TFX_CLEANUP_BYPASS=1 한정). new-tab/split-pane만 차단 유지하려면 아래 조건 세분화.
360
+ if (CLEANUP_BYPASS) {
361
+ // bypass 모드에서는 아래 wt 검사를 이미 통과한 상태. 추가 작업 없음.
362
+ }
363
+
326
364
  // 2. WARN 체크 — allow + additionalContext
327
365
  const warnings = [];
328
366
  for (const rule of WARN_RULES) {