triflux 10.9.21 → 10.9.22

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 (99) 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/hud/constants.mjs +24 -13
  11. package/hud/renderers.mjs +2 -1
  12. package/package.json +62 -21
  13. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  14. package/scripts/__tests__/release-governance.test.mjs +148 -0
  15. package/scripts/doctor-diagnose.mjs +6 -7
  16. package/scripts/lib/cross-review-utils.mjs +2 -2
  17. package/scripts/lib/mcp-filter.mjs +9 -5
  18. package/scripts/release/bump-version.mjs +77 -0
  19. package/scripts/release/check-sync.mjs +51 -0
  20. package/scripts/release/lib.mjs +303 -0
  21. package/scripts/release/prepare.mjs +85 -0
  22. package/scripts/release/publish.mjs +87 -0
  23. package/scripts/release/verify.mjs +81 -0
  24. package/scripts/release/version-manifest.json +26 -0
  25. package/scripts/remote-spawn.mjs +3 -3
  26. package/scripts/setup.mjs +18 -15
  27. package/scripts/tfx-route.sh +64 -8
  28. package/tui/codex-profile.mjs +457 -0
  29. package/tui/core.mjs +266 -0
  30. package/tui/doctor.mjs +375 -0
  31. package/tui/gemini-profile.mjs +299 -0
  32. package/tui/monitor-data.mjs +152 -0
  33. package/tui/monitor.mjs +339 -0
  34. package/tui/setup.mjs +598 -0
  35. package/CLAUDE.md +0 -212
  36. package/references/hosts.json +0 -46
  37. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  38. package/skills/tfx-workspace/evals/evals.json +0 -79
  39. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  40. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  43. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  47. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  50. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  54. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  57. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  61. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  64. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  68. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  69. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  72. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  76. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  79. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  83. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  84. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  87. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  91. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  92. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  93. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  94. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  95. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  96. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  97. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  98. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  99. 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 {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.21",
3
+ "version": "10.9.22",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,26 +13,75 @@
13
13
  "tfx-doctor-tui": "bin/tfx-doctor-tui.mjs",
14
14
  "tfx-setup-tui": "bin/tfx-setup-tui.mjs"
15
15
  },
16
- "engines": {
17
- "node": ">=18.0.0"
18
- },
19
- "dependencies": {
20
- "@triflux/core": "10.0.1",
21
- "@triflux/remote": "^10.0.0-alpha.1"
22
- },
23
16
  "files": [
24
17
  "bin",
18
+ "tui",
19
+ "hub",
20
+ "config",
25
21
  "skills",
22
+ "!skills/tfx-workspace",
23
+ "!**/failure-reports",
24
+ "scripts",
26
25
  "hooks",
27
26
  "hud",
28
- "scripts",
29
- "hub",
30
27
  "mesh",
31
- "references",
32
- "CLAUDE.md",
28
+ ".claude-plugin",
33
29
  "README.md",
30
+ "README.ko.md",
34
31
  "LICENSE"
35
32
  ],
33
+ "workspaces": [
34
+ "packages/core",
35
+ "packages/remote",
36
+ "packages/triflux"
37
+ ],
38
+ "scripts": {
39
+ "pack": "node scripts/pack.mjs all",
40
+ "pack:core": "node scripts/pack.mjs core",
41
+ "pack:remote": "node scripts/pack.mjs remote",
42
+ "setup": "node scripts/setup.mjs",
43
+ "preinstall": "node scripts/preinstall.mjs",
44
+ "postinstall": "node scripts/setup.mjs",
45
+ "lint": "biome check bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
46
+ "lint:fix": "biome check --write bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
47
+ "health": "npm test && npm run lint",
48
+ "test": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
49
+ "test:unit": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/unit/**/*.test.mjs",
50
+ "test:integration": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/integration/**/*.test.mjs",
51
+ "test:route-smoke": "node scripts/test-lock.mjs --test scripts/test-tfx-route-no-claude-native.mjs",
52
+ "test:contract": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/contract/**/*.test.mjs",
53
+ "test:coverage": "node --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=60 --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\"",
54
+ "gen:skill-docs": "node scripts/gen-skill-docs.mjs",
55
+ "gen:skill-manifest": "node scripts/gen-skill-manifest.mjs",
56
+ "release:check-sync": "node scripts/release/check-sync.mjs",
57
+ "release:check-sync:fix": "node scripts/release/check-sync.mjs --fix",
58
+ "release:bump": "node scripts/release/bump-version.mjs",
59
+ "release:prepare": "node scripts/release/prepare.mjs",
60
+ "release:publish": "node scripts/release/publish.mjs",
61
+ "release:verify": "node scripts/release/verify.mjs"
62
+ },
63
+ "engines": {
64
+ "node": ">=18.0.0"
65
+ },
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/tellang/triflux.git"
69
+ },
70
+ "homepage": "https://github.com/tellang/triflux#readme",
71
+ "author": "tellang",
72
+ "license": "MIT",
73
+ "dependencies": {
74
+ "@modelcontextprotocol/sdk": "^1.27.1",
75
+ "better-sqlite3": "^12.6.2",
76
+ "pino": "^10.3.1",
77
+ "pino-pretty": "^13.1.3",
78
+ "systray2": "^2.1.4",
79
+ "zod": "^4.0.0"
80
+ },
81
+ "devDependencies": {
82
+ "@biomejs/biome": "^2.0.0",
83
+ "knip": "^6.3.0"
84
+ },
36
85
  "keywords": [
37
86
  "claude-code",
38
87
  "plugin",
@@ -43,13 +92,5 @@
43
92
  "multi-model",
44
93
  "triflux",
45
94
  "tfx"
46
- ],
47
- "author": "tellang",
48
- "license": "MIT",
49
- "homepage": "https://github.com/tellang/triflux#readme",
50
- "repository": {
51
- "type": "git",
52
- "url": "git+https://github.com/tellang/triflux.git",
53
- "directory": "packages/triflux"
54
- }
95
+ ]
55
96
  }
@@ -34,7 +34,7 @@ const { extractPrompt, sanitizeForKeywordDetection } = detectorModule;
34
34
 
35
35
  function loadCompiledRules() {
36
36
  const rules = loadRules(rulesPath);
37
- assert.equal(rules.length, 32);
37
+ assert.ok(rules.length >= 32);
38
38
  return compileRules(rules);
39
39
  }
40
40
 
@@ -108,8 +108,8 @@ test("sanitizeForKeywordDetection: 코드블록/URL/파일경로/XML 태그 제
108
108
 
109
109
  test("loadRules: 유효한 JSON 로드", () => {
110
110
  const rules = loadRules(rulesPath);
111
- assert.equal(rules.length, 32);
112
- assert.equal(rules.filter((rule) => rule.skill).length, 18);
111
+ assert.ok(rules.length >= 32);
112
+ assert.ok(rules.filter((rule) => rule.skill).length >= 18);
113
113
  assert.equal(rules.filter((rule) => rule.mcp_route).length, 10);
114
114
  });
115
115
 
@@ -130,7 +130,7 @@ test("loadRules: 잘못된 파일 처리", () => {
130
130
  test("compileRules: 정규식 컴파일 성공", () => {
131
131
  const rules = loadRules(rulesPath);
132
132
  const compiled = compileRules(rules);
133
- assert.equal(compiled.length, 32);
133
+ assert.equal(compiled.length, rules.length);
134
134
  for (const rule of compiled) {
135
135
  assert.ok(Array.isArray(rule.compiledPatterns));
136
136
  assert.ok(rule.compiledPatterns.length > 0);