@triflux/remote 10.25.1 → 10.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/hub/server.mjs +21 -8
  2. package/hub/team/agent-map.json +3 -3
  3. package/hub/team/backend.mjs +3 -9
  4. package/hub/team/build-worker-prompt.mjs +33 -2
  5. package/hub/team/claude-daemon-control.mjs +911 -0
  6. package/hub/team/claude-native-bridge.mjs +238 -11
  7. package/hub/team/claude-session-projection.mjs +10 -1
  8. package/hub/team/cli/commands/start/index.mjs +3 -6
  9. package/hub/team/cli/commands/start/parse-args.mjs +10 -3
  10. package/hub/team/cli/commands/start/start-headless.mjs +50 -20
  11. package/hub/team/cli/help.mjs +1 -1
  12. package/hub/team/cli/services/runtime-mode.mjs +6 -3
  13. package/hub/team/execution-mode.mjs +27 -12
  14. package/hub/team/headless.mjs +191 -43
  15. package/hub/team/interactive-native-launcher.mjs +106 -0
  16. package/hub/team/interactive-native-worker.mjs +80 -0
  17. package/hub/team/interactive-tui-transport.mjs +465 -0
  18. package/hub/team/launcher-template.mjs +28 -6
  19. package/hub/team/native.mjs +46 -7
  20. package/hub/team/pane.mjs +0 -5
  21. package/hub/team/psmux.mjs +3 -3
  22. package/hub/team/retry-state-machine.mjs +83 -14
  23. package/hub/team/session.mjs +5 -36
  24. package/hub/team/swarm-hypervisor.mjs +28 -6
  25. package/hub/team/swarm-preflight.mjs +3 -1
  26. package/hub/team/terminal-opener.mjs +1 -1
  27. package/hub/team/tui-core.mjs +4 -0
  28. package/hub/team/tui-lite.mjs +3 -2
  29. package/hub/team/tui-viewer.mjs +4 -4
  30. package/hub/team/tui.mjs +4 -2
  31. package/hub/team/worker-sandbox.mjs +4 -0
  32. package/hub/tools.mjs +4 -1
  33. package/hub/workers/antigravity-route-worker.mjs +169 -0
  34. package/hub/workers/delegator-mcp.mjs +58 -314
  35. package/hub/workers/factory.mjs +7 -4
  36. package/hub/workers/gemini-worker.mjs +55 -306
  37. package/hub/workers/interface.mjs +2 -1
  38. package/hub/workers/worker-utils.mjs +1 -1
  39. package/package.json +1 -1
  40. package/scripts/lib/cli-codex.mjs +3 -3
  41. package/scripts/lib/cli-gemini.mjs +20 -50
  42. package/scripts/lib/keyword-rules.mjs +1 -1
  43. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  44. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  45. package/scripts/lib/psmux-info.mjs +50 -5
@@ -2,11 +2,12 @@
2
2
  // teammate 프롬프트 템플릿 + 팀 설정 빌더
3
3
  //
4
4
  // Claude Code 네이티브 Agent Teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)
5
- // 환경에서 teammate를 Codex/Gemini CLI 래퍼로 구성하는 유틸리티.
5
+ // 환경에서 teammate를 Codex/Antigravity CLI 래퍼로 구성하는 유틸리티.
6
6
  // SKILL.md가 인라인 프롬프트를 사용하므로, 이 모듈은 CLI(tfx multi --native)에서
7
7
  // 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
8
8
 
9
9
  import * as fs from "node:fs/promises";
10
+ import { createRequire } from "node:module";
10
11
  import os from "node:os";
11
12
  import path from "node:path";
12
13
 
@@ -25,6 +26,9 @@ const ROUTE_COMMAND_RE =
25
26
  /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
26
27
  const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
27
28
  const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
29
+ const _require = createRequire(import.meta.url);
30
+ const AGENT_TO_CLI = _require("./agent-map.json");
31
+ const VALID_ROUTE_AGENTS = new Set(Object.keys(AGENT_TO_CLI));
28
32
 
29
33
  function inferWorkerIndex(agentName = "") {
30
34
  const match = /(\d+)(?!.*\d)/.exec(agentName);
@@ -33,13 +37,44 @@ function inferWorkerIndex(agentName = "") {
33
37
  return Number.isInteger(index) && index > 0 ? index : null;
34
38
  }
35
39
 
36
- function buildRouteEnvPrefix(agentName, workerIndex, searchTool) {
40
+ function normalizeRouteMode(cli) {
41
+ const normalized = String(cli || "").trim();
42
+ if (["gemini", "antigravity", "agy"].includes(normalized)) {
43
+ return "antigravity";
44
+ }
45
+ if (normalized === "codex") return "codex";
46
+ return "";
47
+ }
48
+
49
+ function resolveRouteCli(cli) {
50
+ const normalized = String(cli || "").trim();
51
+ const mapped = AGENT_TO_CLI[normalized] || normalized;
52
+ return mapped === "gemini" ? "antigravity" : mapped;
53
+ }
54
+
55
+ function resolveSlimWrapperRouteAgent(cli, role) {
56
+ const routeRole = String(role || "").trim();
57
+ if (
58
+ routeRole &&
59
+ VALID_ROUTE_AGENTS.has(routeRole) &&
60
+ !["gemini", "antigravity", "agy"].includes(routeRole)
61
+ ) {
62
+ return routeRole;
63
+ }
64
+
65
+ const resolvedCli = resolveRouteCli(cli);
66
+ return VALID_ROUTE_AGENTS.has(resolvedCli) ? resolvedCli : "codex";
67
+ }
68
+
69
+ function buildRouteEnvPrefix(agentName, workerIndex, searchTool, cli) {
37
70
  const effectiveWorkerIndex =
38
71
  Number.isInteger(workerIndex) && workerIndex > 0
39
72
  ? workerIndex
40
73
  : inferWorkerIndex(agentName);
74
+ const routeMode = normalizeRouteMode(cli);
41
75
 
42
76
  let envPrefix = "";
77
+ if (routeMode) envPrefix += ` TFX_CLI_MODE="${routeMode}"`;
43
78
  if (effectiveWorkerIndex)
44
79
  envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
45
80
  if (searchTool) envPrefix += ` TFX_SEARCH_TOOL="${searchTool}"`;
@@ -188,7 +223,8 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
188
223
  maxIterations = 3,
189
224
  } = opts;
190
225
 
191
- const routeTimeoutSec = getRouteTimeout(role, mcp_profile);
226
+ const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
227
+ const routeTimeoutSec = getRouteTimeout(routeAgent, mcp_profile);
192
228
  const escaped = subtask.replace(/'/g, "'\\''");
193
229
  const pipelineHint = pipelinePhase
194
230
  ? `\n파이프라인 단계: ${pipelinePhase}`
@@ -197,6 +233,7 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
197
233
  agentName,
198
234
  workerIndex,
199
235
  searchTool,
236
+ cli,
200
237
  );
201
238
  const scoutConstraint =
202
239
  role === "scout" || role === "scientist"
@@ -218,14 +255,14 @@ SendMessage(type: "message", recipient: "${leadName}", content: "작업 시작:
218
255
 
219
256
  [HARD CONSTRAINT] 허용 도구: Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용한다.
220
257
  Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
221
- 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Gemini에 위임하라.
258
+ 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
222
259
  이 규칙을 위반하면 작업 실패로 간주한다.${scoutConstraint}
223
260
 
224
261
  gemini/codex를 직접 호출하지 마라. 반드시 tfx-route.sh를 거쳐야 한다.
225
262
  프롬프트를 파일로 저장하지 마라. tfx-route.sh가 인자로 받는다.
226
263
 
227
264
  Step 1 — Async 시작 (즉시 리턴, <1초):
228
- Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} --async "${role}" '"'"'${escaped}'"'"' ${mcp_profile} ${routeTimeoutSec}', timeout: ${launchTimeoutMs})
265
+ Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} --async "${routeAgent}" '"'"'${escaped}'"'"' ${mcp_profile} ${routeTimeoutSec}', timeout: ${launchTimeoutMs})
229
266
  → 출력 한 줄이 JOB_ID이다. 반드시 기억하라.
230
267
 
231
268
  Step 2 — 완료 대기 (내부 폴링, 최대 540초):
@@ -333,9 +370,11 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
333
370
  agentName,
334
371
  workerIndex,
335
372
  searchTool,
373
+ cli,
336
374
  );
375
+ const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
337
376
 
338
- const routeCmd = `TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} "${role}" '${escaped}' ${mcp_profile}`;
377
+ const routeCmd = `TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} "${routeAgent}" '${escaped}' ${mcp_profile}`;
339
378
 
340
379
  return `하이브리드 psmux 워커 프로토콜:
341
380
 
@@ -360,7 +399,7 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
360
399
  ${pipelineHint}
361
400
  [HARD CONSTRAINT] 너는 Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용할 수 있다.
362
401
  Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
363
- 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Gemini에 위임하라.
402
+ 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
364
403
  이 규칙을 위반하면 작업 실패로 간주한다.
365
404
 
366
405
  gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통해 실행한다.
package/hub/team/pane.mjs CHANGED
@@ -31,11 +31,6 @@ function toMuxPath(p) {
31
31
  const drive = m[1].toLowerCase();
32
32
  const rest = m[2];
33
33
 
34
- // wsl tmux는 /mnt/c/... 경로를 사용
35
- if (mux === "wsl-tmux") {
36
- return `/mnt/${drive}/${rest}`;
37
- }
38
-
39
34
  // Git Bash/MSYS tmux는 /c/... 경로를 사용
40
35
  return `/${drive}/${rest}`;
41
36
  }
@@ -234,7 +234,7 @@ function ensurePsmuxInstalled() {
234
234
  if (!hasPsmux()) {
235
235
  throw new Error(
236
236
  "psmux가 설치되어 있지 않습니다.\n\n" +
237
- "psmux는 Codex/Gemini CLI를 병렬 세션으로 실행하는 터미널 멀티플렉서입니다.\n" +
237
+ "psmux는 Codex/Antigravity CLI를 병렬 세션으로 실행하는 터미널 멀티플렉서입니다.\n" +
238
238
  "설치 방법 (택 1):\n" +
239
239
  `${formatPsmuxInstallGuidance(" ")}\n\n` +
240
240
  "설치 후 터미널을 재시작하세요.",
@@ -1412,10 +1412,10 @@ export function startCapture(sessionName, paneNameOrTarget) {
1412
1412
  * @returns {{ paneId: string, paneName: string, token: string, logPath: string }}
1413
1413
  */
1414
1414
  /**
1415
- * CLI 명령(codex/gemini)이 psmux pane의 PowerShell 환경에서 단축 플래그 충돌을
1415
+ * CLI 명령(codex/legacy Gemini)이 psmux pane의 PowerShell 환경에서 단축 플래그 충돌을
1416
1416
  * 일으키는 문제를 방지하기 위해 bash -c '...' 로 감싼다.
1417
1417
  * - codex -o flag → PS -OutVariable/OutBuffer 충돌
1418
- * - gemini --prompt flag (v8.6.0: -p → --prompt, PS 충돌 해소)
1418
+ * - legacy Gemini prompt flag (v8.6.0: -p → long-form prompt flag, PS 충돌 해소)
1419
1419
  * @param {string} cmd
1420
1420
  * @returns {string}
1421
1421
  */
@@ -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 });
@@ -38,20 +38,6 @@ function hasTmux() {
38
38
  }
39
39
  }
40
40
 
41
- /** WSL2 내 tmux 사용 가능 여부 (Windows 전용) */
42
- function hasWslTmux() {
43
- try {
44
- execSync("wsl tmux -V", {
45
- stdio: "ignore",
46
- timeout: 5000,
47
- windowsHide: true,
48
- });
49
- return true;
50
- } catch {
51
- return false;
52
- }
53
- }
54
-
55
41
  /** Git Bash 내 tmux 사용 가능 여부 (Windows 전용) */
56
42
  function hasGitBashTmux() {
57
43
  const bash = resolveGitBashExecutable();
@@ -103,7 +89,7 @@ function hasLiteralPsmuxBinary(command = process.env.PSMUX_BIN || "psmux") {
103
89
 
104
90
  /**
105
91
  * 터미널 멀티플렉서 감지 (결과 캐싱 — 프로세스 수명 동안 불변)
106
- * @returns {'tmux'|'git-bash-tmux'|'wsl-tmux'|'psmux'|null}
92
+ * @returns {'tmux'|'git-bash-tmux'|'psmux'|null}
107
93
  */
108
94
  let _cachedMux;
109
95
  export function detectMultiplexer() {
@@ -139,16 +125,12 @@ export function detectMultiplexer() {
139
125
  _cachedMux = "git-bash-tmux";
140
126
  return _cachedMux;
141
127
  }
142
- if (hasWslTmux()) {
143
- _cachedMux = "wsl-tmux";
144
- return _cachedMux;
145
- }
146
128
  _cachedMux = null;
147
129
  return _cachedMux;
148
130
  }
149
131
 
150
132
  /**
151
- * tmux/psmux 커맨드 실행 (wsl-tmux 투명 지원)
133
+ * tmux/psmux 커맨드 실행
152
134
  * @param {string} args — tmux 서브커맨드 + 인자
153
135
  * @param {object} opts — execSync 옵션
154
136
  * @returns {string} stdout
@@ -159,14 +141,9 @@ function tmux(args, opts = {}) {
159
141
  throw new Error(
160
142
  "tmux/psmux 미발견.\n\n" +
161
143
  "tfx multi은 tmux 계열 멀티플렉서가 필요합니다:\n" +
162
- " Windows: psmux 설치 또는 WSL2 tmux 사용\n" +
163
- " WSL2: wsl sudo apt install tmux\n" +
144
+ " Windows: psmux 설치\n" +
164
145
  " macOS: brew install tmux\n" +
165
- " Linux: apt install tmux\n\n" +
166
- "Windows에서는 WSL2를 권장합니다:\n" +
167
- " 1. wsl --install\n" +
168
- " 2. wsl sudo apt install tmux\n" +
169
- ' 3. tfx multi "작업" (자동으로 WSL tmux 사용)',
146
+ " Linux: apt install tmux",
170
147
  );
171
148
  }
172
149
  if (mux === "psmux") {
@@ -190,8 +167,7 @@ function tmux(args, opts = {}) {
190
167
  return (r.stdout || "").trim();
191
168
  }
192
169
 
193
- const prefix = mux === "wsl-tmux" ? "wsl tmux" : "tmux";
194
- const result = execSync(`${prefix} ${args}`, {
170
+ const result = execSync(`tmux ${args}`, {
195
171
  encoding: "utf8",
196
172
  timeout: 10000,
197
173
  stdio: ["pipe", "pipe", "pipe"],
@@ -238,13 +214,6 @@ export function resolveAttachCommand(sessionName, opts = {}) {
238
214
  };
239
215
  }
240
216
 
241
- if (mux === "wsl-tmux") {
242
- return {
243
- command: "wsl",
244
- args: ["tmux", "attach-session", "-t", sessionName],
245
- };
246
- }
247
-
248
217
  return {
249
218
  command: "tmux",
250
219
  args: ["attach-session", "-t", sessionName],
@@ -156,6 +156,19 @@ function resolveHubStatusUrl(hub) {
156
156
  return `http://${formatHubHostForUrl(host)}:${port}/status`;
157
157
  }
158
158
 
159
+ function isTruthyConfigValue(value) {
160
+ if (typeof value === "boolean") return value;
161
+ return /^(true|yes|1|interactive)$/i.test(String(value || "").trim());
162
+ }
163
+
164
+ function isInteractiveNativeBridgeShard(shard) {
165
+ return (
166
+ isTruthyConfigValue(shard?.interactive) ||
167
+ isTruthyConfigValue(shard?.nativeBridgeInteractive) ||
168
+ String(shard?.workerType || "").toLowerCase() === "interactive"
169
+ );
170
+ }
171
+
159
172
  /**
160
173
  * Create a swarm hypervisor.
161
174
  * @param {object} opts
@@ -591,6 +604,7 @@ export function createSwarmHypervisor(opts) {
591
604
  async function maybeRegisterNativeBridgeShard(shard, sessionConfig) {
592
605
  if (!nativeBridge) return null;
593
606
  try {
607
+ const interactive = !shard.host && isInteractiveNativeBridgeShard(shard);
594
608
  const registration = await registerSwarmShardImpl({
595
609
  sessionId: sessionConfig.id,
596
610
  cli: shard.agent,
@@ -598,8 +612,11 @@ export function createSwarmHypervisor(opts) {
598
612
  swarmName: runId,
599
613
  ...getShardDisplayPosition(shard),
600
614
  shardName: shard.name,
601
- cwd: workdir,
615
+ cwd: interactive
616
+ ? shard.worktreePath || sessionConfig.workdir || workdir
617
+ : workdir,
602
618
  host: shard.host || "local",
619
+ interactive,
603
620
  _deps: {
604
621
  warn(message) {
605
622
  emitter.emit("warning", {
@@ -646,7 +663,7 @@ export function createSwarmHypervisor(opts) {
646
663
  agent: shard.agent,
647
664
  // #125: append Completion Protocol appendix so workers emit a
648
665
  // sentinel-framed JSON payload that conductor can reliably capture.
649
- prompt: buildWorkerPrompt(shard.prompt),
666
+ prompt: buildWorkerPrompt(shard.prompt, { leaseFiles: shard.files }),
650
667
  workdir: shard.worktreePath || workdir,
651
668
  mcpServers: shard.mcp,
652
669
  worktreePath: shard.worktreePath || null,
@@ -917,10 +934,15 @@ export function createSwarmHypervisor(opts) {
917
934
  const worker = workers.get(shardName);
918
935
  const shard = plan?.shards.find((item) => item.name === shardName);
919
936
  if (worker && shard) {
920
- const cleanupPromise = maybeCleanupWorktree(shardName, worker, shard, {
921
- force: true,
922
- failureReason: FAILURE_MODES.F7_WORKER_DID_NOT_COMMIT,
923
- });
937
+ const cleanupPromise = maybeCleanupWorktree(
938
+ shardName,
939
+ worker,
940
+ shard,
941
+ {
942
+ force: true,
943
+ failureReason: FAILURE_MODES.F7_WORKER_DID_NOT_COMMIT,
944
+ },
945
+ );
924
946
  worker.cleanupPromise = cleanupPromise;
925
947
  void cleanupPromise
926
948
  .finally(() => {
@@ -10,7 +10,9 @@ import { planSwarm } from "./swarm-planner.mjs";
10
10
 
11
11
  const AGENT_COMMANDS = Object.freeze({
12
12
  codex: "codex",
13
- gemini: "gemini",
13
+ gemini: "agy",
14
+ antigravity: "agy",
15
+ agy: "agy",
14
16
  claude: "claude",
15
17
  });
16
18
 
@@ -4,7 +4,7 @@ import { psmuxExec as defaultPsmuxExec } from "./psmux.mjs";
4
4
  import { tmuxExec as defaultTmuxExec, detectMultiplexer } from "./session.mjs";
5
5
  import { createWtManager as defaultCreateWtManager } from "./wt-manager.mjs";
6
6
 
7
- const TMUX_LIKE_MUXES = new Set(["tmux", "wsl-tmux", "git-bash-tmux"]);
7
+ const TMUX_LIKE_MUXES = new Set(["tmux", "git-bash-tmux"]);
8
8
 
9
9
  export function sanitizeTerminalTitle(value, fallback = "triflux") {
10
10
  const title = String(value ?? "")
@@ -137,6 +137,10 @@ export function normalizeWorkerState(existing = {}, state = {}, opts = {}) {
137
137
  : existing.cli || "codex",
138
138
  role:
139
139
  state.role !== undefined ? sanitizeOneLine(state.role) : existing.role,
140
+ displayName:
141
+ state.displayName !== undefined
142
+ ? sanitizeOneLine(state.displayName, existing.displayName)
143
+ : existing.displayName,
140
144
  status:
141
145
  state.status !== undefined
142
146
  ? sanitizeOneLine(state.status, existing.status || "pending")
@@ -82,6 +82,7 @@ function buildWorkerRail(names, workers, selectedWorker, width) {
82
82
  const lines = names.length
83
83
  ? names.map((name, index) => {
84
84
  const worker = workers.get(name);
85
+ const displayName = worker?.displayName || name;
85
86
  const status = runtimeStatus(worker);
86
87
  const pct = Math.round(
87
88
  ((worker?.progress ?? (status === "completed" ? 1 : 0)) || 0) * 100,
@@ -91,7 +92,7 @@ function buildWorkerRail(names, workers, selectedWorker, width) {
91
92
  : "";
92
93
  const prefix =
93
94
  name === selectedWorker ? color("▶", MOCHA.blue) : dim("·");
94
- return `${prefix} ${index + 1}.${name} ${stripAnsi(statusBadge(status))} ${pct}%${token}`;
95
+ return `${prefix} ${index + 1}.${displayName} ${stripAnsi(statusBadge(status))} ${pct}%${token}`;
95
96
  })
96
97
  : [dim("workers 없음")];
97
98
  return frame(lines, width);
@@ -115,7 +116,7 @@ function buildDetail(workerName, worker, width, tab, helpVisible) {
115
116
  }
116
117
  if (!workerName || !worker) return frame([dim("선택된 워커 없음")], width);
117
118
  const detailLines = [
118
- bold(workerName),
119
+ bold(worker.displayName || workerName),
119
120
  `status ${runtimeStatus(worker)}`,
120
121
  `progress ${Math.round((worker.progress || 0) * 100)}% ${progressBar(Math.round((worker.progress || 0) * 100), 12)}`,
121
122
  `tokens ${formatTokens(worker.tokens)}`,
@@ -447,10 +447,10 @@ function ingest() {
447
447
  function pushToTui(paneName, cli, paneTitle, update) {
448
448
  // _leadAction은 tui에 노출하지 않음 (내부용)
449
449
  const { _leadAction: _ignored, ...safeUpdate } = update;
450
- // pane title에서 실제 역할만 추출: "⚪ codex (executor)" → "executor"
451
- const roleMatch = paneTitle.match(/\(([^)]+)\)$/);
452
- const role = roleMatch ? roleMatch[1] : "";
453
- tui.updateWorker(paneName, { cli, role, ...safeUpdate });
450
+ // pane title의 괄호 값은 UI 라벨이며 role 의미값으로 승격하지 않는다.
451
+ const labelMatch = paneTitle.match(/\(([^)]+)\)$/);
452
+ const displayName = labelMatch ? labelMatch[1] : paneName;
453
+ tui.updateWorker(paneName, { cli, displayName, role: "", ...safeUpdate });
454
454
  }
455
455
 
456
456
  // ── render 루프 (8-12FPS ≈ 100ms) ──
package/hub/team/tui.mjs CHANGED
@@ -573,6 +573,7 @@ function buildWorkerRail(name, st, opts = {}) {
573
573
  const innerWidth = Math.max(12, width - 4);
574
574
  const cli = st.cli || "codex";
575
575
  const role = sanitizeOneLine(st.role);
576
+ const displayName = sanitizeOneLine(st.displayName, name);
576
577
  const status = runtimeStatus(st);
577
578
  const sec = Number.isFinite(st._logSec) ? st._logSec : 0;
578
579
  const changeElapsed = st._statusChangedAt
@@ -598,9 +599,9 @@ function buildWorkerRail(name, st, opts = {}) {
598
599
  st.host && st.host !== "local"
599
600
  ? color(`[${st.host}]`, MOCHA.mauve) + " "
600
601
  : "";
601
- const displayRole = dedupeRole(role, name, cli);
602
+ const displayRole = dedupeRole(role, displayName, cli);
602
603
  const title = truncate(
603
- `${selMark} ${hb} ${hostBadge}${color(name, FG.triflux)} ${color("•", MOCHA.overlay)} ${color(cli, cliColor(cli))}${displayRole ? ` ${color(`(${displayRole})`, MOCHA.overlay)}` : ""}`,
604
+ `${selMark} ${hb} ${hostBadge}${color(displayName, FG.triflux)} ${color("•", MOCHA.overlay)} ${color(cli, cliColor(cli))}${displayRole ? ` ${color(`(${displayRole})`, MOCHA.overlay)}` : ""}`,
604
605
  innerWidth,
605
606
  );
606
607
 
@@ -1658,6 +1659,7 @@ export function createLogDashboard(opts = {}) {
1658
1659
  cli: merged.cli,
1659
1660
  status: merged.status,
1660
1661
  role: merged.role,
1662
+ displayName: merged.displayName,
1661
1663
  snapshot: merged.snapshot,
1662
1664
  summary: merged.summary,
1663
1665
  detail: merged.detail,
@@ -71,6 +71,10 @@ export function buildWorkerSandboxEnv(opts = {}) {
71
71
  XDG_CACHE_HOME: xdgCache,
72
72
  XDG_STATE_HOME: xdgState,
73
73
  };
74
+ const hostHome = env.HOME || env.USERPROFILE;
75
+ if (!env.CODEX_HOME && hostHome) {
76
+ overlay.CODEX_HOME = join(hostHome, ".codex");
77
+ }
74
78
 
75
79
  const winParts =
76
80
  windowsHomeParts(home) ||
package/hub/tools.mjs CHANGED
@@ -135,7 +135,10 @@ export function createTools(store, router, hitl, pipe = null) {
135
135
  ],
136
136
  properties: {
137
137
  agent_id: { type: "string", pattern: "^[a-zA-Z0-9._:-]{3,64}$" },
138
- cli: { type: "string", enum: ["codex", "gemini", "claude", "other"] },
138
+ cli: {
139
+ type: "string",
140
+ enum: ["codex", "antigravity", "gemini", "claude", "other"],
141
+ },
139
142
  pid: { type: "integer", minimum: 1 },
140
143
  capabilities: {
141
144
  type: "array",