@triflux/core 10.25.1 → 10.27.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.
@@ -21,7 +21,7 @@ import {
21
21
  readFileSync,
22
22
  writeFileSync,
23
23
  } from "node:fs";
24
- import { dirname } from "node:path";
24
+ import { dirname, join } from "node:path";
25
25
 
26
26
  export const STATES = Object.freeze({
27
27
  PLANNING: "PLANNING",
@@ -38,16 +38,12 @@ export const MODES = Object.freeze({
38
38
  ESCALATE: "auto-escalate",
39
39
  });
40
40
 
41
- // Escalation chain (2026-04-25 정책):
42
- // 1. gpt-5.4-mini — 비용 최저, fast tier, 단순 task 대부분 해결
43
- // 2. gpt-5.3-codex — 가성비 중간, code specialized (Plus/free 모두 OK, fast 미지원)
44
- // 3. gpt-5.5 — top reasoning + 코드 강함, fast tier
45
- // 4. claude opus-4-7 — 최종 수단
46
- // sonnet-4-6 단계는 제거: gpt-5.5 가 코드/추론/비용 모두 우위 + 5.3-codex 가성비
47
- // 단계가 더 적합한 중간 격상.
41
+ const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
42
+
43
+ // Escalation chain (2026-05-27 정책):
44
+ // 1. codex gpt-5.5 — 코드/추론/비용에서 mini/5.3-codex 중간 단계를 대체
45
+ // 2. claude opus-4-7 — 최종 수단
48
46
  const DEFAULT_ESCALATION_CHAIN = Object.freeze([
49
- Object.freeze({ cli: "codex", model: "gpt-5.4-mini" }),
50
- Object.freeze({ cli: "codex", model: "gpt-5.3-codex" }),
51
47
  Object.freeze({ cli: "codex", model: "gpt-5.5" }),
52
48
  Object.freeze({ cli: "claude", model: "opus-4-7" }),
53
49
  ]);
@@ -56,9 +52,7 @@ const STUCK_THRESHOLD = 3;
56
52
 
57
53
  export function createRetryStateMachine(options = {}) {
58
54
  const mode = options.mode || MODES.BOUNDED;
59
- const cliChain = Array.isArray(options.cliChain)
60
- ? options.cliChain.slice()
61
- : DEFAULT_ESCALATION_CHAIN.slice();
55
+ const cliChain = resolveEscalationChain(options);
62
56
  const maxIterationsInput =
63
57
  typeof options.maxIterations === "number" ? options.maxIterations : null;
64
58
  const maxIterations =
@@ -205,7 +199,10 @@ export function createRetryStateMachine(options = {}) {
205
199
  state.lastFailureReason = snapshot.lastFailureReason || null;
206
200
  state.cliIndex = Number(snapshot.cliIndex) || 0;
207
201
  if (Array.isArray(snapshot.cliChain) && snapshot.cliChain.length > 0) {
208
- state.cliChain = snapshot.cliChain.slice();
202
+ state.cliChain = normalizeEscalationChain(
203
+ snapshot.cliChain,
204
+ "snapshot.cliChain",
205
+ );
209
206
  }
210
207
  if (snapshot.mode) state.mode = snapshot.mode;
211
208
  if (snapshot.sessionId !== undefined) state.sessionId = snapshot.sessionId;
@@ -227,6 +224,78 @@ export function createRetryStateMachine(options = {}) {
227
224
  };
228
225
  }
229
226
 
227
+ function resolveEscalationChain(options) {
228
+ if (Array.isArray(options.cliChain)) {
229
+ return normalizeEscalationChain(options.cliChain, "options.cliChain");
230
+ }
231
+
232
+ const projectRoot = options.projectRoot || process.cwd();
233
+ return (
234
+ loadEscalationChainOverride(projectRoot) ||
235
+ normalizeEscalationChain(
236
+ DEFAULT_ESCALATION_CHAIN,
237
+ "DEFAULT_ESCALATION_CHAIN",
238
+ )
239
+ );
240
+ }
241
+
242
+ export function loadEscalationChainOverride(projectRoot = process.cwd()) {
243
+ const configPath = join(projectRoot, ESCALATION_CHAIN_CONFIG_PATH);
244
+ if (!existsSync(configPath)) return null;
245
+
246
+ const raw = readFileSync(configPath, "utf8");
247
+ let parsed;
248
+ try {
249
+ parsed = JSON.parse(raw);
250
+ } catch (err) {
251
+ throw new Error(
252
+ `escalation-chain override 파싱 실패: ${configPath} — 원인: ${err.message} — 수정: JSON 문법 확인 또는 파일 삭제로 DEFAULT_ESCALATION_CHAIN 사용`,
253
+ { cause: err },
254
+ );
255
+ }
256
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
257
+ throw new Error(`${configPath}: expected object`);
258
+ }
259
+ if (parsed.version !== undefined && parsed.version !== 1) {
260
+ throw new Error(`${configPath}: unsupported version ${parsed.version}`);
261
+ }
262
+ return normalizeEscalationChain(parsed.chain, configPath);
263
+ }
264
+
265
+ function normalizeEscalationChain(chain, source) {
266
+ if (!Array.isArray(chain) || chain.length === 0) {
267
+ throw new Error(`${source}: chain must be a non-empty array`);
268
+ }
269
+ return chain.map((entry, index) =>
270
+ normalizeEscalationChainEntry(entry, index, source),
271
+ );
272
+ }
273
+
274
+ function normalizeEscalationChainEntry(entry, index, source) {
275
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
276
+ throw new Error(`${source}: chain[${index}] must be an object`);
277
+ }
278
+ if (typeof entry.cli !== "string" || entry.cli.trim() === "") {
279
+ throw new Error(
280
+ `${source}: chain[${index}].cli must be a non-empty string`,
281
+ );
282
+ }
283
+ if (typeof entry.model !== "string" || entry.model.trim() === "") {
284
+ throw new Error(
285
+ `${source}: chain[${index}].model must be a non-empty string`,
286
+ );
287
+ }
288
+
289
+ const normalized = {
290
+ cli: entry.cli,
291
+ model: entry.model,
292
+ };
293
+ if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
294
+ normalized.profile = entry.profile;
295
+ }
296
+ return normalized;
297
+ }
298
+
230
299
  function persistTransition(stateFile, entry) {
231
300
  const dir = dirname(stateFile);
232
301
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -1,5 +1,5 @@
1
1
  // hub/workers/worker-utils.mjs — 워커 공통 유틸리티
2
- // claude-worker, gemini-worker, pipe 등에서 공유하는 순수 유틸 함수 모음.
2
+ // claude-worker, Antigravity route compatibility worker, pipe 등에서 공유하는 순수 유틸 함수 모음.
3
3
 
4
4
  export const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
5
5
  export const DEFAULT_KILL_GRACE_MS = 1000;
package/hud/constants.mjs CHANGED
@@ -138,6 +138,11 @@ export const SPAWN_LOCK_TTL_MS = 30 * 1000; // 30초 spawn dedup
138
138
 
139
139
  // Gemini 쿼터 API 관련
140
140
  export const GEMINI_OAUTH_PATH = join(homedir(), ".gemini", "oauth_creds.json");
141
+ export const ANTIGRAVITY_OAUTH_PATHS = [
142
+ join(homedir(), ".gemini", "antigravity-cli", "oauth_creds.json"),
143
+ join(homedir(), ".gemini", "antigravity-cli", "credentials.json"),
144
+ GEMINI_OAUTH_PATH,
145
+ ];
141
146
  export const GEMINI_QUOTA_CACHE_PATH = join(
142
147
  homedir(),
143
148
  ".claude",
@@ -162,6 +167,7 @@ export const TFX_PREFLIGHT_CACHE_PATH = join(
162
167
  "cache",
163
168
  "tfx-preflight.json",
164
169
  );
170
+ export const TFX_PREFLIGHT_CACHE_STALE_MS = 60 * 60 * 1000;
165
171
  export const GEMINI_RPM_TRACKER_PATH = join(
166
172
  homedir(),
167
173
  ".claude",
@@ -26,6 +26,7 @@ import {
26
26
  GEMINI_SESSION_REFRESH_FLAG,
27
27
  QOS_PATH,
28
28
  TFX_PREFLIGHT_CACHE_PATH,
29
+ TFX_PREFLIGHT_CACHE_STALE_MS,
29
30
  } from "./constants.mjs";
30
31
  import {
31
32
  buildContextUsageView,
@@ -50,6 +51,7 @@ import {
50
51
  import {
51
52
  buildGeminiAuthContext,
52
53
  fetchGeminiQuota,
54
+ getAntigravityEmail,
53
55
  getGeminiEmail,
54
56
  readGeminiQuotaSnapshot,
55
57
  readGeminiSessionSnapshot,
@@ -111,10 +113,12 @@ async function main() {
111
113
 
112
114
  const qosProfile = readJson(QOS_PATH, { providers: {} });
113
115
  const preflightCache = readJson(TFX_PREFLIGHT_CACHE_PATH, null);
116
+ const preflightTimestamp = Number(preflightCache?.timestamp);
117
+ const preflightFresh =
118
+ Number.isFinite(preflightTimestamp) &&
119
+ Date.now() - preflightTimestamp <= TFX_PREFLIGHT_CACHE_STALE_MS;
114
120
  const antigravityReady =
115
- process.env.TFX_ANTIGRAVITY_OK === "1" ||
116
- preflightCache?.antigravity?.ok === true ||
117
- preflightCache?.available_agents?.includes("antigravity");
121
+ preflightFresh && preflightCache?.antigravity?.ok === true;
118
122
  const accountsConfig = readJson(ACCOUNTS_CONFIG_PATH, { providers: {} });
119
123
  const accountsState = readJson(ACCOUNTS_STATE_PATH, { providers: {} });
120
124
  const claudeUsageSnapshot = readClaudeUsageSnapshot();
@@ -150,6 +154,7 @@ async function main() {
150
154
  const claudeUsage = claudeUsageSnapshot.data;
151
155
  const codexEmail = getCodexEmail();
152
156
  const geminiEmail = getGeminiEmail();
157
+ const antigravityEmail = getAntigravityEmail();
153
158
  const codexBuckets = codexSnapshot.buckets;
154
159
  const geminiSession = geminiSessionSnapshot.session;
155
160
  const geminiQuota = geminiQuotaSnapshot.quota;
@@ -206,9 +211,9 @@ async function main() {
206
211
  contextView,
207
212
  claudeUsage,
208
213
  codexBuckets,
209
- geminiSession,
210
- geminiBucket,
211
- combinedSvPct,
214
+ antigravityReady ? null : geminiSession,
215
+ antigravityReady ? null : geminiBucket,
216
+ antigravityReady ? Math.round((codexSv ?? 0) * 100) : combinedSvPct,
212
217
  antigravityReady ? "a" : "g",
213
218
  );
214
219
  process.stdout.write(`\x1b[0m${microLine}\n`);
@@ -252,9 +257,9 @@ async function main() {
252
257
  qosProfile,
253
258
  accountsConfig,
254
259
  accountsState,
255
- geminiQuotaData,
256
- antigravityReady ? "anti" : geminiEmail,
257
- geminiSv,
260
+ antigravityReady ? { type: "antigravity" } : geminiQuotaData,
261
+ antigravityReady ? antigravityEmail : geminiEmail,
262
+ antigravityReady ? null : geminiSv,
258
263
  null,
259
264
  ),
260
265
  ];
@@ -284,7 +289,6 @@ async function main() {
284
289
  // 비활성 프로바이더 dim 처리: 데이터 없으면 전체 줄 dim
285
290
  const codexActive = codexBuckets != null;
286
291
  const geminiActive =
287
- antigravityReady ||
288
292
  (geminiSession?.total || 0) > 0 ||
289
293
  geminiBucket != null ||
290
294
  geminiProBucket != null ||
@@ -8,6 +8,7 @@ import https from "node:https";
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  import {
11
+ ANTIGRAVITY_OAUTH_PATHS,
11
12
  GEMINI_API_TIMEOUT_MS,
12
13
  GEMINI_OAUTH_PATH,
13
14
  GEMINI_PROJECT_CACHE_PATH,
@@ -82,15 +83,39 @@ export function deriveGeminiLimits(bucket) {
82
83
  };
83
84
  }
84
85
 
86
+ function getCredentialEmail(credential) {
87
+ return (
88
+ decodeJwtEmail(credential?.id_token) ||
89
+ credential?.email ||
90
+ credential?.account?.email ||
91
+ credential?.user?.email ||
92
+ credential?.profile?.email ||
93
+ null
94
+ );
95
+ }
96
+
85
97
  export function getGeminiEmail() {
86
98
  try {
87
99
  const oauth = readJson(GEMINI_OAUTH_PATH, null);
88
- return decodeJwtEmail(oauth?.id_token);
100
+ return getCredentialEmail(oauth);
89
101
  } catch {
90
102
  return null;
91
103
  }
92
104
  }
93
105
 
106
+ export function getAntigravityEmail() {
107
+ for (const oauthPath of ANTIGRAVITY_OAUTH_PATHS) {
108
+ try {
109
+ const oauth = readJson(oauthPath, null);
110
+ const email = getCredentialEmail(oauth);
111
+ if (email) return email;
112
+ } catch {
113
+ // Try the next known Antigravity credential path.
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
94
119
  export function buildGeminiAuthContext(accountId) {
95
120
  const oauth = readJson(GEMINI_OAUTH_PATH, null);
96
121
  const tokenSource =
package/hud/renderers.mjs CHANGED
@@ -434,6 +434,16 @@ export function getAccountLabel(
434
434
  return label;
435
435
  }
436
436
 
437
+ function formatAntigravityQuotaSection(currentTier) {
438
+ if (currentTier === "minimal") {
439
+ return `${dim("q:")}${dim(formatPlaceholderPercentCell())}`;
440
+ }
441
+ if (currentTier === "compact") {
442
+ return `${dim("q:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
443
+ }
444
+ return `${dim("q:")}${tierDimBar(currentTier)}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
445
+ }
446
+
437
447
  export function getProviderRow(
438
448
  currentTier,
439
449
  provider,
@@ -470,6 +480,9 @@ export function getProviderRow(
470
480
 
471
481
  if (currentTier === "nano" || currentTier === "micro") {
472
482
  const minPrefix = `${bold(markerColor(`${marker}`))}:`;
483
+ if (provider === "antigravity") {
484
+ return { prefix: minPrefix, left: dim("--"), right: "" };
485
+ }
473
486
  if (realQuota?.type === "codex") {
474
487
  const main =
475
488
  realQuota.buckets.codex ||
@@ -498,7 +511,7 @@ export function getProviderRow(
498
511
  };
499
512
  }
500
513
  }
501
- if (realQuota?.type === "gemini") {
514
+ if (provider === "gemini" && realQuota?.type === "gemini") {
502
515
  const pools = realQuota.pools || {};
503
516
  if (pools.pro || pools.flash) {
504
517
  const pP = pools.pro
@@ -526,6 +539,9 @@ export function getProviderRow(
526
539
  }
527
540
 
528
541
  if (currentTier === "minimal") {
542
+ if (provider === "antigravity") {
543
+ quotaSection = formatAntigravityQuotaSection(currentTier);
544
+ }
529
545
  if (realQuota?.type === "codex") {
530
546
  const main =
531
547
  realQuota.buckets.codex ||
@@ -550,7 +566,7 @@ export function getProviderRow(
550
566
  quotaSection = `${dim("5h:")}${fCell} ${dim("1w:")}${wCell}`;
551
567
  }
552
568
  }
553
- if (realQuota?.type === "gemini") {
569
+ if (provider === "gemini" && realQuota?.type === "gemini") {
554
570
  const pools = realQuota.pools || {};
555
571
  if (pools.pro || pools.flash) {
556
572
  const slot = (bucket, label) => {
@@ -579,6 +595,9 @@ export function getProviderRow(
579
595
  }
580
596
 
581
597
  if (currentTier === "compact") {
598
+ if (provider === "antigravity") {
599
+ quotaSection = formatAntigravityQuotaSection(currentTier);
600
+ }
582
601
  if (realQuota?.type === "codex") {
583
602
  const main =
584
603
  realQuota.buckets.codex ||
@@ -610,7 +629,7 @@ export function getProviderRow(
610
629
  quotaSection = `${dim("5h:")}${fCell} ${dim(formatTimeCell(fiveReset))} ${dim("1w:")}${wCell} ${dim(formatTimeCellDH(weekReset))}`;
611
630
  }
612
631
  }
613
- if (realQuota?.type === "gemini") {
632
+ if (provider === "gemini" && realQuota?.type === "gemini") {
614
633
  const pools = realQuota.pools || {};
615
634
  const hasAnyPool = pools.pro || pools.flash;
616
635
  if (hasAnyPool) {
@@ -644,6 +663,10 @@ export function getProviderRow(
644
663
  }
645
664
 
646
665
  // full tier
666
+ if (provider === "antigravity") {
667
+ quotaSection = formatAntigravityQuotaSection(currentTier);
668
+ }
669
+
647
670
  if (realQuota?.type === "codex") {
648
671
  const main =
649
672
  realQuota.buckets.codex ||
@@ -686,7 +709,7 @@ export function getProviderRow(
686
709
  }
687
710
  }
688
711
 
689
- if (realQuota?.type === "gemini") {
712
+ if (provider === "gemini" && realQuota?.type === "gemini") {
690
713
  const pools = realQuota.pools || {};
691
714
  const hasAnyPool = pools.pro || pools.flash;
692
715
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.25.1",
3
+ "version": "10.27.0",
4
4
  "description": "triflux core — CLI routing, pipeline, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -33,14 +33,14 @@ const AGENT_PROFILES = {
33
33
  mcpHint: "implement",
34
34
  },
35
35
  cleanup: {
36
- profile: "mini54_med",
36
+ profile: "gpt55_med",
37
37
  timeoutSec: 540,
38
38
  runMode: "fg",
39
39
  opusOversight: false,
40
40
  mcpHint: "implement",
41
41
  },
42
42
  deslop: {
43
- profile: "mini54_med",
43
+ profile: "gpt55_med",
44
44
  timeoutSec: 540,
45
45
  runMode: "fg",
46
46
  opusOversight: false,
@@ -157,7 +157,7 @@ const AGENT_PROFILES = {
157
157
  subcommand: "review",
158
158
  },
159
159
  spark: {
160
- profile: "spark53_low",
160
+ profile: "gpt55_low",
161
161
  timeoutSec: 180,
162
162
  runMode: "fg",
163
163
  opusOversight: false,
@@ -1,39 +1,11 @@
1
- // cli-gemini.mjs — Gemini CLI adapter (Phase 0 PoC).
2
- //
3
- // 책임: Gemini 프로파일/모델/timeout 매핑. resolve_gemini_profile() 의 결과는 Phase 1 에서
4
- // 통합한다 (Phase 0 은 라벨만 전달).
5
- //
6
- // 출처: scripts/tfx-route.sh route_agent() L1045-L1050.
1
+ // cli-gemini.mjs — legacy Gemini adapter alias.
2
+ // The public "gemini" label remains accepted, but the executable plan is AGY.
7
3
 
8
- export const id = "gemini";
9
- export const cliType = "gemini";
10
- export const command = "gemini";
11
-
12
- const AGENT_PROFILES = {
13
- designer: {
14
- profile: "pro31",
15
- timeoutSec: 900,
16
- runMode: "bg",
17
- opusOversight: false,
18
- mcpHint: "docs",
19
- },
20
- gemini: {
21
- profile: "pro31",
22
- timeoutSec: 900,
23
- runMode: "bg",
24
- opusOversight: false,
25
- mcpHint: "docs",
26
- },
27
- writer: {
28
- profile: "flash3",
29
- timeoutSec: 900,
30
- runMode: "bg",
31
- opusOversight: false,
32
- mcpHint: "docs",
33
- },
34
- };
4
+ import * as agyAdapter from "./cli-agy.mjs";
35
5
 
36
- const DEFAULT_PROFILE = AGENT_PROFILES.gemini;
6
+ export const id = "gemini";
7
+ export const cliType = "antigravity";
8
+ export const command = "agy";
37
9
 
38
10
  export function plan({
39
11
  agent,
@@ -45,23 +17,21 @@ export function plan({
45
17
  if (!agent) {
46
18
  throw new Error("[cli-gemini] agent required");
47
19
  }
48
- const cfg = AGENT_PROFILES[agent] ?? DEFAULT_PROFILE;
49
- const effectiveTimeoutSec =
50
- Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : cfg.timeoutSec;
51
- return {
52
- command,
53
- profile: cfg.profile,
54
- effort: cfg.profile,
55
- args: ["-m", cfg.profile, "-y", "--prompt"],
56
- timeoutMs: effectiveTimeoutSec * 1000,
57
- runMode: cfg.runMode,
58
- opusOversight: cfg.opusOversight,
59
- mcpProfile: mcpProfile === "auto" ? cfg.mcpHint : mcpProfile,
60
- promptLength: prompt.length,
61
- contextFile: contextFile || null,
62
- };
20
+ return agyAdapter.plan({
21
+ agent: agent === "gemini" ? "antigravity" : agent,
22
+ prompt,
23
+ mcpProfile,
24
+ timeoutSec,
25
+ contextFile,
26
+ });
63
27
  }
64
28
 
65
29
  export function describe() {
66
- return { id, cliType, command, agents: Object.keys(AGENT_PROFILES) };
30
+ return {
31
+ id,
32
+ cliType,
33
+ command,
34
+ aliases: ["gemini"],
35
+ agents: ["gemini", "designer", "writer", "antigravity", "agy"],
36
+ };
67
37
  }
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
- const VALID_MCP_ROUTES = new Set(["codex", "gemini", "claude"]);
3
+ const VALID_MCP_ROUTES = new Set(["codex", "antigravity", "gemini", "claude"]);
4
4
 
5
5
  function logRuleError(message, error) {
6
6
  if (error) {
@@ -12,14 +12,14 @@ export const PSMUX_OPTIONAL_COMMANDS = ["detach-client"];
12
12
 
13
13
  // Windows 패키지 매니저 — winget/scoop/choco 는 Windows 전용
14
14
  export const PSMUX_INSTALL_COMMANDS = [
15
- "winget install marlocarlo.psmux",
15
+ "winget install psmux",
16
16
  "scoop install psmux",
17
17
  "choco install psmux",
18
18
  "cargo install psmux",
19
19
  ];
20
20
 
21
21
  export const PSMUX_UPDATE_COMMANDS = [
22
- "winget upgrade marlocarlo.psmux",
22
+ "winget upgrade psmux",
23
23
  "scoop update psmux",
24
24
  "choco upgrade psmux",
25
25
  "cargo install psmux --force",
@@ -42,14 +42,15 @@ export const PSMUX_INSTALL_COMMANDS_LINUX = ["cargo install psmux"];
42
42
  export const PSMUX_UPDATE_COMMANDS_LINUX = ["cargo install psmux --force"];
43
43
 
44
44
  // PR #258 OS-primary 분기 정책: macOS/Linux 는 tmux 가 POSIX 표준 primary,
45
- // psmux 는 선택형. native teams fallback (tmux 또는 child process spawn) 으로 동작 가능.
45
+ // psmux 는 선택형이다. 따라서 macOS/Linux 에서 "psmux 미설치"는 fallback 사유가
46
+ // 아니며, tmux 까지 없을 때만 native/in-process fallback 을 고려한다.
46
47
  export const PSMUX_FALLBACK_NOTE_DARWIN =
47
48
  "macOS 에서는 tmux 가 표준 멀티플렉서이며, " +
48
- "triflux 는 psmux 없이도 native teams fallback 으로 멀티모델 병렬 실행을 지원합니다.";
49
+ "psmux 미설치는 fallback 사유가 아닙니다. tmux 도 없을 때만 in-process/native fallback 을 사용합니다.";
49
50
 
50
51
  export const PSMUX_FALLBACK_NOTE_LINUX =
51
52
  "Linux 에서는 tmux 가 표준 멀티플렉서이며, " +
52
- "triflux 는 psmux 없이도 native teams fallback 으로 멀티모델 병렬 실행을 지원합니다.";
53
+ "psmux 미설치는 fallback 사유가 아닙니다. tmux 도 없을 때만 in-process/native fallback 을 사용합니다.";
53
54
 
54
55
  export function getPsmuxInstallCommandsFor(platform = process.platform) {
55
56
  if (platform === "darwin") return PSMUX_INSTALL_COMMANDS_DARWIN;
@@ -177,3 +178,47 @@ export function probePsmuxSupport(options = {}) {
177
178
  };
178
179
  }
179
180
  }
181
+
182
+ export function probePrimaryMultiplexerSupport(options = {}) {
183
+ const platform = options.platform || process.platform;
184
+ const execFileSyncFn = options.execFileSyncFn || execFileSync;
185
+
186
+ if (platform === "win32") {
187
+ return {
188
+ kind: "psmux",
189
+ ...probePsmuxSupport(options),
190
+ };
191
+ }
192
+
193
+ const bin = options.bin || "tmux";
194
+ try {
195
+ const versionOutput = execFileSyncFn(bin, ["-V"], {
196
+ encoding: "utf8",
197
+ timeout: 2000,
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ windowsHide: true,
200
+ });
201
+ return {
202
+ ok: true,
203
+ installed: true,
204
+ kind: "tmux",
205
+ version: String(versionOutput || "").trim() || null,
206
+ recommendedVersion: null,
207
+ recommended: true,
208
+ missingCommands: [],
209
+ missingOptionalCommands: [],
210
+ };
211
+ } catch (error) {
212
+ return {
213
+ ok: false,
214
+ installed: false,
215
+ kind: "tmux",
216
+ version: null,
217
+ recommendedVersion: null,
218
+ recommended: false,
219
+ missingCommands: ["tmux"],
220
+ missingOptionalCommands: [],
221
+ error: error?.message || String(error),
222
+ };
223
+ }
224
+ }