triflux 10.9.21 → 10.9.23

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 (100) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/config/mcp-registry.json +29 -0
  4. package/hub/account-broker.mjs +6 -4
  5. package/hub/cli-adapter-base.mjs +14 -14
  6. package/hub/lib/env-detect.mjs +47 -20
  7. package/hub/server.mjs +17 -15
  8. package/hub/team/headless.mjs +10 -0
  9. package/hub/team/swarm-hypervisor.mjs +2 -2
  10. package/hub/workers/delegator-mcp.mjs +129 -1
  11. package/hud/constants.mjs +24 -13
  12. package/hud/renderers.mjs +2 -1
  13. package/package.json +62 -21
  14. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  15. package/scripts/__tests__/release-governance.test.mjs +148 -0
  16. package/scripts/doctor-diagnose.mjs +6 -7
  17. package/scripts/lib/cross-review-utils.mjs +2 -2
  18. package/scripts/lib/mcp-filter.mjs +12 -24
  19. package/scripts/release/bump-version.mjs +77 -0
  20. package/scripts/release/check-sync.mjs +51 -0
  21. package/scripts/release/lib.mjs +303 -0
  22. package/scripts/release/prepare.mjs +85 -0
  23. package/scripts/release/publish.mjs +87 -0
  24. package/scripts/release/verify.mjs +81 -0
  25. package/scripts/release/version-manifest.json +26 -0
  26. package/scripts/remote-spawn.mjs +3 -3
  27. package/scripts/setup.mjs +18 -15
  28. package/scripts/tfx-route.sh +64 -8
  29. package/tui/codex-profile.mjs +457 -0
  30. package/tui/core.mjs +266 -0
  31. package/tui/doctor.mjs +375 -0
  32. package/tui/gemini-profile.mjs +299 -0
  33. package/tui/monitor-data.mjs +152 -0
  34. package/tui/monitor.mjs +339 -0
  35. package/tui/setup.mjs +598 -0
  36. package/CLAUDE.md +0 -212
  37. package/references/hosts.json +0 -46
  38. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  39. package/skills/tfx-workspace/evals/evals.json +0 -79
  40. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  43. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  47. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  50. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  54. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  57. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  61. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  64. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  68. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  69. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  72. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  76. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  79. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  83. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  84. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  87. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  91. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  92. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  93. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  94. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  95. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  96. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  97. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  98. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  99. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  100. 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.21",
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.21"
34
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "triflux",
3
+ "version": "10.9.22",
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
+ }
@@ -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
+ }
@@ -120,11 +120,13 @@ class AccountBroker extends EventEmitter {
120
120
  #config;
121
121
  #state; // Map<accountId, accountState>
122
122
  #roundRobinIndex; // Map<provider, number>
123
+ #persist; // boolean — disable persistence for tests
123
124
 
124
- constructor(config) {
125
+ constructor(config, { _skipPersistence = false } = {}) {
125
126
  super();
126
127
  const parsed = ConfigSchema.parse(config);
127
128
  this.#config = parsed;
129
+ this.#persist = !_skipPersistence;
128
130
 
129
131
  this.#state = new Map();
130
132
  this.#roundRobinIndex = new Map();
@@ -134,7 +136,7 @@ class AccountBroker extends EventEmitter {
134
136
  ...(parsed.gemini || []).map((a) => ({ ...a, provider: "gemini" })),
135
137
  ];
136
138
 
137
- const persisted = loadPersistedState();
139
+ const persisted = this.#persist ? loadPersistedState() : null;
138
140
  const pEntries = persisted?.entries || {};
139
141
 
140
142
  for (const account of allAccounts) {
@@ -386,7 +388,7 @@ class AccountBroker extends EventEmitter {
386
388
  };
387
389
 
388
390
  this.#state.set(accountId, updated);
389
- persistState(this.#state);
391
+ if (this.#persist) persistState(this.#state);
390
392
  this.emit("release", { id: accountId, ok });
391
393
  }
392
394
 
@@ -401,7 +403,7 @@ class AccountBroker extends EventEmitter {
401
403
  leasedAt: null,
402
404
  cooldownUntil: Date.now() + coolMs,
403
405
  });
404
- persistState(this.#state);
406
+ if (this.#persist) persistState(this.#state);
405
407
  }
406
408
 
407
409
  // ── snapshot ──────────────────────────────────────────────────
@@ -218,14 +218,15 @@ export async function executeWithCircuitBroker({
218
218
  const { withRetry } = await import("./workers/worker-utils.mjs");
219
219
 
220
220
  // access broker as live binding property (not destructured) so reloadBroker() propagates
221
- const lease = brokerMod.broker?.lease({ provider });
222
- if (!lease) {
221
+ const hasBroker = brokerMod.broker != null;
222
+ const lease = hasBroker ? brokerMod.broker.lease({ provider }) : null;
223
+ if (hasBroker && !lease) {
223
224
  return createResult(false, { fellBack: true, failureMode: "circuit_open" });
224
225
  }
225
226
 
226
227
  const preflight = await preflightFn(opts);
227
228
  if (!preflight.ok) {
228
- brokerMod.broker.release(lease.id, { ok: false });
229
+ if (lease) brokerMod.broker.release(lease.id, { ok: false });
229
230
  return createResult(false, {
230
231
  stderr: appendWarnings("", preflight.warnings),
231
232
  fellBack: opts.fallbackToClaude !== false,
@@ -273,20 +274,19 @@ export async function executeWithCircuitBroker({
273
274
  }
274
275
 
275
276
  if (lastResult.ok) {
276
- brokerMod.broker.release(lease.id, { ok: true });
277
+ if (lease) brokerMod.broker.release(lease.id, { ok: true });
277
278
  return lastResult;
278
279
  }
279
280
 
280
- if (lastResult.failureMode === "rate_limited") {
281
- const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
282
- const coolMs = parseRetryAfterMs(text, provider);
283
- brokerMod.broker.markRateLimited(lease.id, coolMs);
284
- brokerMod.broker.emit("cooldown", { id: lease.id, provider, coolMs, reason: "quota_exhausted" });
285
- } else if (lastResult.failureMode === "crash") {
286
- // 인프라 에러(모듈 누락, 서버 에러 등)는 계정 문제가 아님 → circuit/cooldown 건너뜀
287
- brokerMod.broker.release(lease.id, { ok: false, skipCircuit: true });
288
- } else {
289
- brokerMod.broker.release(lease.id, { ok: false });
281
+ if (lease) {
282
+ if (lastResult.failureMode === "rate_limited") {
283
+ const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
284
+ const coolMs = parseRetryAfterMs(text, provider);
285
+ brokerMod.broker.markRateLimited(lease.id, coolMs);
286
+ brokerMod.broker.emit("cooldown", { id: lease.id, provider, coolMs, reason: "quota_exhausted" });
287
+ } else {
288
+ brokerMod.broker.release(lease.id, { ok: false });
289
+ }
290
290
  }
291
291
  return {
292
292
  ...lastResult,
@@ -3,68 +3,95 @@ import { execFileSync } from "node:child_process";
3
3
  import { platform as osPlatform } from "node:os";
4
4
 
5
5
  let _cached = null;
6
+ let _shellCache = null;
7
+ let _terminalCache = null;
8
+ let _multiplexerCache = null;
9
+
10
+ const PIPE_OPTS = { encoding: "utf8", timeout: 3000, stdio: "pipe" };
6
11
 
7
12
  /**
8
13
  * 기본 쉘 감지
9
- * Windows: pwsh → powershell
10
- * Unix: $SHELL → /bin/sh
14
+ * Windows: pwsh → powershell (full path + version)
15
+ * Unix: $SHELL → /bin/sh (+ version)
11
16
  */
12
17
  export function detectShell() {
18
+ if (_shellCache) return _shellCache;
19
+
13
20
  const platform = osPlatform();
21
+
14
22
  if (platform === "win32") {
15
23
  try {
16
- execFileSync("where", ["pwsh.exe"], { stdio: "ignore", timeout: 3000 });
17
- return { name: "pwsh", path: "pwsh.exe", version: null };
24
+ const path = execFileSync("where", ["pwsh.exe"], PIPE_OPTS).trim().split(/\r?\n/)[0];
25
+ let version = null;
26
+ try {
27
+ version = execFileSync(path, ["-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"], PIPE_OPTS).trim();
28
+ } catch {}
29
+ _shellCache = { name: "pwsh", path, version };
18
30
  } catch {
19
31
  try {
20
- execFileSync("where", ["powershell.exe"], { stdio: "ignore", timeout: 3000 });
21
- return { name: "powershell", path: "powershell.exe", version: null };
32
+ const path = execFileSync("where", ["powershell.exe"], PIPE_OPTS).trim().split(/\r?\n/)[0];
33
+ _shellCache = { name: "powershell", path, version: null };
22
34
  } catch {
23
- return { name: "cmd", path: "cmd.exe", version: null, installHint: "pwsh: winget install Microsoft.PowerShell" };
35
+ _shellCache = { name: "powershell", path: "", version: null, installHint: "pwsh: winget install Microsoft.PowerShell" };
24
36
  }
25
37
  }
38
+ return _shellCache;
26
39
  }
27
40
 
28
41
  const shellPath = process.env.SHELL || "/bin/sh";
29
42
  const name = shellPath.split("/").pop() || "sh";
30
- return { name, path: shellPath, version: null };
43
+ let version = null;
44
+ try {
45
+ version = execFileSync(shellPath, ["--version"], PIPE_OPTS).trim();
46
+ } catch {}
47
+ _shellCache = { name, path: shellPath, version };
48
+ return _shellCache;
31
49
  }
32
50
 
33
51
  /**
34
52
  * 터미널 에뮬레이터 감지
35
53
  */
36
54
  export function detectTerminal() {
55
+ if (_terminalCache) return _terminalCache;
56
+
37
57
  const platform = osPlatform();
38
58
  if (platform === "win32") {
39
59
  try {
40
- execFileSync("where", ["wt.exe"], { stdio: "ignore", timeout: 3000 });
41
- return { name: "windows-terminal", hasWt: true };
60
+ execFileSync("where", ["wt.exe"], PIPE_OPTS);
61
+ _terminalCache = { name: "windows-terminal", hasWt: true };
42
62
  } catch {
43
- return { name: "conhost", hasWt: false, installHint: "Windows Terminal: winget install Microsoft.WindowsTerminal" };
63
+ _terminalCache = { name: "unknown", hasWt: false, installHint: "wt: winget install Microsoft.WindowsTerminal" };
44
64
  }
65
+ return _terminalCache;
45
66
  }
46
67
 
47
68
  if (process.env.TERM_PROGRAM === "iTerm.app") {
48
- return { name: "iterm2", hasWt: false };
49
- }
50
- if (process.env.TERM_PROGRAM === "Apple_Terminal") {
51
- return { name: "terminal-app", hasWt: false };
69
+ _terminalCache = { name: "iterm2", hasWt: false };
70
+ } else if (process.env.TERM_PROGRAM === "Apple_Terminal") {
71
+ _terminalCache = { name: "terminal-app", hasWt: false };
72
+ } else {
73
+ _terminalCache = { name: "unknown", hasWt: false };
52
74
  }
53
-
54
- return { name: "unknown", hasWt: false };
75
+ return _terminalCache;
55
76
  }
56
77
 
57
78
  /**
58
79
  * 멀티플렉서 감지 (tmux)
59
80
  */
60
81
  export function detectMultiplexer() {
82
+ if (_multiplexerCache) return _multiplexerCache;
83
+
61
84
  try {
62
85
  const cmd = osPlatform() === "win32" ? "where" : "which";
63
- const path = execFileSync(cmd, ["tmux"], { encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"] }).trim();
64
- return { name: "tmux", path };
86
+ const path = execFileSync(cmd, ["tmux"], PIPE_OPTS).trim();
87
+ _multiplexerCache = { name: "tmux", path };
65
88
  } catch {
66
- return { name: "none", path: null };
89
+ const hint = osPlatform() === "win32"
90
+ ? "tmux: install tmux in WSL or MSYS2"
91
+ : undefined;
92
+ _multiplexerCache = { name: "none", path: null, ...(hint ? { installHint: hint } : {}) };
67
93
  }
94
+ return _multiplexerCache;
68
95
  }
69
96
 
70
97
  /**
package/hub/server.mjs CHANGED
@@ -80,7 +80,7 @@ const AIMD_WINDOW_MS = 30 * 60 * 1000;
80
80
  const AIMD_INITIAL_BATCH_SIZE = 3;
81
81
  const AIMD_MIN_BATCH_SIZE = 1;
82
82
  const AIMD_MAX_BATCH_SIZE = 10;
83
- const HUB_IDLE_TIMEOUT_DEFAULT_MS = 10 * 60 * 1000;
83
+ const HUB_IDLE_TIMEOUT_DEFAULT_MS = 0; // 0 = 영구 실행 (idle shutdown 비활성). TFX_HUB_IDLE_TIMEOUT_MS 환경변수로 오버라이드 가능
84
84
  const HUB_IDLE_SWEEP_DEFAULT_MS = 60 * 1000;
85
85
  const STATIC_CONTENT_TYPES = Object.freeze({
86
86
  ".html": "text/html",
@@ -1655,21 +1655,23 @@ export async function startHub({
1655
1655
  return stopPromise;
1656
1656
  };
1657
1657
 
1658
- idleTimer = setInterval(() => {
1659
- const idleMs = Date.now() - lastRequestAt;
1660
- if (idleMs < hubIdleTimeoutMs) return;
1661
- hubLog.warn(
1662
- { idleMs, idleTimeoutMs: hubIdleTimeoutMs, port },
1663
- "hub.idle_timeout_shutdown",
1664
- );
1665
- void stopFn().catch((error) => {
1666
- hubLog.error(
1667
- { err: error, idleMs, idleTimeoutMs: hubIdleTimeoutMs, port },
1668
- "hub.idle_timeout_shutdown_failed",
1658
+ if (hubIdleTimeoutMs > 0) {
1659
+ idleTimer = setInterval(() => {
1660
+ const idleMs = Date.now() - lastRequestAt;
1661
+ if (idleMs < hubIdleTimeoutMs) return;
1662
+ hubLog.warn(
1663
+ { idleMs, idleTimeoutMs: hubIdleTimeoutMs, port },
1664
+ "hub.idle_timeout_shutdown",
1669
1665
  );
1670
- });
1671
- }, hubIdleSweepMs);
1672
- idleTimer.unref();
1666
+ void stopFn().catch((error) => {
1667
+ hubLog.error(
1668
+ { err: error, idleMs, idleTimeoutMs: hubIdleTimeoutMs, port },
1669
+ "hub.idle_timeout_shutdown_failed",
1670
+ );
1671
+ });
1672
+ }, hubIdleSweepMs);
1673
+ idleTimer.unref();
1674
+ }
1673
1675
 
1674
1676
  resolveHub({
1675
1677
  reused: false,
@@ -161,6 +161,16 @@ const MCP_PROFILE_HINTS = {
161
161
  * @param {string} [opts.contextFile] — 컨텍스트 파일 경로 (최대 32KB, UTF-8 안전 절단)
162
162
  * @returns {string} PowerShell 명령
163
163
  */
164
+ // ── Dashboard attach args for WT ────────────────────────────────
165
+
166
+ export function buildDashboardAttachArgs(sessionName, layout, workerCount, anchor = "window") {
167
+ const safeName = String(sessionName).replace(/[^a-zA-Z0-9_-]/g, "");
168
+ const base = anchor === "tab"
169
+ ? ["-w", "0", "nt"]
170
+ : ["-w", "new"];
171
+ return [...base, "--session", safeName, "--layout", layout, "--workers", String(workerCount)];
172
+ }
173
+
164
174
  export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
165
175
  const { handoff = true, mcp, contextFile, model, cwd } = opts;
166
176
  const resolvedCli = resolveCliType(cli);
@@ -1065,10 +1065,10 @@ export function createSwarmHypervisor(opts) {
1065
1065
  * @param {SwarmPlan} swarmPlan — from planSwarm()
1066
1066
  * @returns {Promise<SwarmStatus>}
1067
1067
  */
1068
- /** Hub keepalive — 스웜 실행 Hub idle timeout 방지 */
1068
+ /** Hub keepalive — Hub crash recovery (idle timeout 기본 비활성이므로 crash 복구 용도) */
1069
1069
  let hubKeepaliveTimer = null;
1070
1070
  function startHubKeepalive() {
1071
- // 5분마다 Hub /status 핑 (idle timeout 기본 10분)
1071
+ // 5분마다 Hub /status 핑 crash 감지 시 ensureHubAlive로 재시작
1072
1072
  hubKeepaliveTimer = setInterval(
1073
1073
  async () => {
1074
1074
  try {
@@ -14,6 +14,7 @@ import * as z from "zod";
14
14
  import { resolveBashExecutable } from "../lib/bash-path.mjs";
15
15
  import { CodexMcpWorker } from "./codex-mcp.mjs";
16
16
  import { GeminiWorker } from "./gemini-worker.mjs";
17
+ import { runHeadlessWithCleanup } from "../team/headless.mjs";
17
18
 
18
19
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
19
20
 
@@ -321,6 +322,18 @@ const DelegateInputSchema = z.object({
321
322
  teamAgentName: z.string().optional().describe("TFX_TEAM_AGENT_NAME"),
322
323
  teamLeadName: z.string().optional().describe("TFX_TEAM_LEAD_NAME"),
323
324
  hubUrl: z.string().optional().describe("TFX_HUB_URL"),
325
+ workers: z
326
+ .array(
327
+ z.object({
328
+ provider: z.enum(["codex", "gemini"]).describe("워커 provider"),
329
+ agentType: z.string().default("executor").describe("역할명"),
330
+ prompt: z.string().describe("워커별 프롬프트"),
331
+ mcpProfile: z.string().default("auto").describe("MCP 프로필"),
332
+ model: z.string().optional().describe("모델 오버라이드"),
333
+ }),
334
+ )
335
+ .optional()
336
+ .describe("병렬 멀티워커 목록. 지정 시 psmux 기반 병렬 실행"),
324
337
  });
325
338
 
326
339
  const DelegateStatusInputSchema = z.object({
@@ -341,7 +354,7 @@ const DelegateOutputSchema = z.object({
341
354
  jobId: z.string().optional(),
342
355
  job_id: z.string().optional(),
343
356
  mode: z.enum(["sync", "async"]).optional(),
344
- status: z.enum(["running", "completed", "failed"]).optional(),
357
+ status: z.enum(["running", "completed", "failed", "partial"]).optional(),
345
358
  error: z.string().optional(),
346
359
  providerRequested: z.string().optional(),
347
360
  providerResolved: z.string().nullable().optional(),
@@ -357,6 +370,20 @@ const DelegateOutputSchema = z.object({
357
370
  threadId: z.string().nullable().optional(),
358
371
  sessionKey: z.string().nullable().optional(),
359
372
  conversationOpen: z.boolean().optional(),
373
+ workerResults: z
374
+ .array(
375
+ z.object({
376
+ cli: z.string(),
377
+ role: z.string().optional(),
378
+ paneName: z.string(),
379
+ matched: z.boolean(),
380
+ exitCode: z.number().nullable(),
381
+ output: z.string(),
382
+ }),
383
+ )
384
+ .optional()
385
+ .describe("멀티워커 개별 결과"),
386
+ sessionName: z.string().optional().describe("psmux 세션명"),
360
387
  });
361
388
 
362
389
  function isTeamRouteRequested(args) {
@@ -791,6 +818,23 @@ export class DelegatorMcpWorker {
791
818
  "위임 실행을 시작합니다.",
792
819
  );
793
820
 
821
+ // 멀티워커 분기: workers 배열이 있으면 headless psmux 병렬 실행
822
+ if (Array.isArray(args.workers) && args.workers.length > 0) {
823
+ try {
824
+ const result = await this._executeMultiWorker(args, extra);
825
+ await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, "위임이 완료되었습니다.");
826
+ return result;
827
+ } catch (error) {
828
+ const message = error instanceof Error ? error.message : String(error);
829
+ return createErrorPayload(message, {
830
+ mode: "sync",
831
+ providerRequested: "multi",
832
+ agentType: args.agentType,
833
+ transport: "headless-psmux",
834
+ });
835
+ }
836
+ }
837
+
794
838
  const runViaRoute = this._shouldUseRoute(args);
795
839
 
796
840
  try {
@@ -816,6 +860,90 @@ export class DelegatorMcpWorker {
816
860
  }
817
861
  }
818
862
 
863
+ async _executeMultiWorker(args, extra) {
864
+ await emitProgress(
865
+ extra,
866
+ DIRECT_PROGRESS_START,
867
+ 100,
868
+ "멀티워커 psmux 병렬 실행을 시작합니다.",
869
+ );
870
+
871
+ // workers 배열을 headless assignments 형식으로 변환
872
+ const assignments = args.workers.map((w) => {
873
+ // provider를 CLI 이름으로 매핑 (codex/gemini 그대로)
874
+ const cli = w.provider;
875
+ const role = w.agentType || "executor";
876
+ // buildDirectPrompt와 동일한 context 처리
877
+ const prompt = withContext(String(w.prompt || ""), args.contextFile);
878
+ return { cli, prompt, role };
879
+ });
880
+
881
+ // 타임아웃: 워커 중 가장 긴 타임아웃 사용
882
+ const maxTimeoutSec = Math.max(
883
+ ...args.workers.map((w) =>
884
+ Math.ceil(resolveTimeoutMs(w.agentType || "executor", args.timeoutMs) / 1000),
885
+ ),
886
+ 300,
887
+ );
888
+
889
+ try {
890
+ const { results, sessionName } = await runHeadlessWithCleanup(assignments, {
891
+ sessionPrefix: "dlg",
892
+ timeoutSec: maxTimeoutSec,
893
+ layout: assignments.length <= 2 ? "even-horizontal" : "2x2",
894
+ progressive: true,
895
+ dashboard: true,
896
+ dashboardLayout: "single",
897
+ });
898
+
899
+ await emitProgress(
900
+ extra,
901
+ DIRECT_PROGRESS_DONE,
902
+ 100,
903
+ `멀티워커 실행 완료: ${results.length}개 워커`,
904
+ );
905
+
906
+ // 전체 성공 판단: 모든 워커가 matched && exitCode === 0
907
+ const allOk = results.every((r) => r.matched && r.exitCode === 0);
908
+ // 개별 출력을 합산
909
+ const combinedOutput = results
910
+ .map(
911
+ (r, i) =>
912
+ `=== Worker ${i + 1} (${r.cli}/${assignments[i].role}) ===\n${r.output || "(no output)"}`,
913
+ )
914
+ .join("\n\n");
915
+
916
+ return {
917
+ ok: allOk,
918
+ mode: "sync",
919
+ status: allOk ? "completed" : "partial",
920
+ providerRequested: "multi",
921
+ providerResolved: "multi",
922
+ agentType: args.agentType || "executor",
923
+ transport: "headless-psmux",
924
+ exitCode: allOk ? 0 : 1,
925
+ output: combinedOutput,
926
+ workerResults: results.map((r) => ({
927
+ cli: r.cli,
928
+ role: r.role || "",
929
+ paneName: r.paneName,
930
+ matched: r.matched,
931
+ exitCode: r.exitCode,
932
+ output: r.output || "",
933
+ })),
934
+ sessionName,
935
+ };
936
+ } catch (error) {
937
+ const message = error instanceof Error ? error.message : String(error);
938
+ return createErrorPayload(message, {
939
+ mode: "sync",
940
+ providerRequested: "multi",
941
+ agentType: args.agentType || "executor",
942
+ transport: "headless-psmux",
943
+ });
944
+ }
945
+ }
946
+
819
947
  async _executeWorker(args, extra) {
820
948
  await emitProgress(
821
949
  extra,
package/hud/constants.mjs CHANGED
@@ -8,20 +8,23 @@ export const VERSION = "2.0";
8
8
 
9
9
  export const QOS_PATH = join(
10
10
  homedir(),
11
- ".omc",
12
- "state",
11
+ ".claude",
12
+ "cache",
13
+ "tfx-hub",
13
14
  "cli_qos_profile.json",
14
15
  );
15
16
  export const ACCOUNTS_CONFIG_PATH = join(
16
17
  homedir(),
17
- ".omc",
18
- "router",
18
+ ".claude",
19
+ "cache",
20
+ "tfx-hub",
19
21
  "accounts.json",
20
22
  );
21
23
  export const ACCOUNTS_STATE_PATH = join(
22
24
  homedir(),
23
- ".omc",
24
- "state",
25
+ ".claude",
26
+ "cache",
27
+ "tfx-hub",
25
28
  "cli_accounts_state.json",
26
29
  );
27
30
 
@@ -46,7 +49,13 @@ export const CONTEXT_MONITOR_LEGACY_PATH = join(
46
49
  "state",
47
50
  "context-monitor.json",
48
51
  );
49
- export const CONTEXT_MONITOR_LOG_DIR = join(homedir(), ".omc", "logs");
52
+ export const CONTEXT_MONITOR_LOG_DIR = join(
53
+ homedir(),
54
+ ".claude",
55
+ "cache",
56
+ "tfx-hub",
57
+ "logs",
58
+ );
50
59
 
51
60
  // 원격 프로브 캐시 (tfx-remote-spawn)
52
61
  export const REMOTE_ENV_CACHE_DIR = join(
@@ -70,15 +79,17 @@ export const CLAUDE_USAGE_CACHE_PATH = join(
70
79
  "cache",
71
80
  "claude-usage-cache.json",
72
81
  );
73
- export const OMC_PLUGIN_USAGE_CACHE_PATH = join(
82
+ export const PLUGIN_USAGE_CACHE_PATH = join(
74
83
  homedir(),
75
84
  ".claude",
76
- "plugins",
77
- "oh-my-claudecode",
78
- ".usage-cache.json",
85
+ "cache",
86
+ "tfx-hub",
87
+ "plugin-usage-cache.json",
79
88
  );
80
- export const CLAUDE_USAGE_STALE_MS_SOLO = 5 * 60 * 1000; // OMC 없을 때: 5분 캐시
81
- export const CLAUDE_USAGE_STALE_MS_WITH_OMC = 15 * 60 * 1000; // OMC 있을 때: 15(OMC가 30초마다 갱신)
89
+ export const OMC_PLUGIN_USAGE_CACHE_PATH = PLUGIN_USAGE_CACHE_PATH; // OMC alias
90
+ export const CLAUDE_USAGE_STALE_MS_SOLO = 5 * 60 * 1000; // 플러그인 없을 때: 5캐시
91
+ export const CLAUDE_USAGE_STALE_MS_WITH_PLUGIN = 15 * 60 * 1000; // 플러그인 있을 때: 15분
92
+ export const CLAUDE_USAGE_STALE_MS_WITH_OMC = CLAUDE_USAGE_STALE_MS_WITH_PLUGIN; // OMC alias
82
93
  export const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
83
94
  export const GEMINI_429_BASE_DELAY_MS = 2000;
84
95
  export const GEMINI_429_MAX_RETRIES = 3;
package/hud/renderers.mjs CHANGED
@@ -389,7 +389,8 @@ export function getClaudeRows(
389
389
  }
390
390
 
391
391
  if (currentTier === "minimal") {
392
- const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}`;
392
+ const staleTag = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
393
+ const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleTag}`;
393
394
  const right = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
394
395
  return [{ prefix, left: quotaSection, right }];
395
396
  }