@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.
- package/hub/server.mjs +21 -8
- package/hub/team/agent-map.json +3 -3
- package/hub/team/backend.mjs +3 -9
- package/hub/team/build-worker-prompt.mjs +33 -2
- package/hub/team/claude-daemon-control.mjs +911 -0
- package/hub/team/claude-native-bridge.mjs +238 -11
- package/hub/team/claude-session-projection.mjs +10 -1
- package/hub/team/cli/commands/start/index.mjs +3 -6
- package/hub/team/cli/commands/start/parse-args.mjs +10 -3
- package/hub/team/cli/commands/start/start-headless.mjs +50 -20
- package/hub/team/cli/help.mjs +1 -1
- package/hub/team/cli/services/runtime-mode.mjs +6 -3
- package/hub/team/execution-mode.mjs +27 -12
- package/hub/team/headless.mjs +191 -43
- package/hub/team/interactive-native-launcher.mjs +106 -0
- package/hub/team/interactive-native-worker.mjs +80 -0
- package/hub/team/interactive-tui-transport.mjs +465 -0
- package/hub/team/launcher-template.mjs +28 -6
- package/hub/team/native.mjs +46 -7
- package/hub/team/pane.mjs +0 -5
- package/hub/team/psmux.mjs +3 -3
- package/hub/team/retry-state-machine.mjs +83 -14
- package/hub/team/session.mjs +5 -36
- package/hub/team/swarm-hypervisor.mjs +28 -6
- package/hub/team/swarm-preflight.mjs +3 -1
- package/hub/team/terminal-opener.mjs +1 -1
- package/hub/team/tui-core.mjs +4 -0
- package/hub/team/tui-lite.mjs +3 -2
- package/hub/team/tui-viewer.mjs +4 -4
- package/hub/team/tui.mjs +4 -2
- package/hub/team/worker-sandbox.mjs +4 -0
- package/hub/tools.mjs +4 -1
- package/hub/workers/antigravity-route-worker.mjs +169 -0
- package/hub/workers/delegator-mcp.mjs +58 -314
- package/hub/workers/factory.mjs +7 -4
- package/hub/workers/gemini-worker.mjs +55 -306
- package/hub/workers/interface.mjs +2 -1
- package/hub/workers/worker-utils.mjs +1 -1
- package/package.json +1 -1
- package/scripts/lib/cli-codex.mjs +3 -3
- package/scripts/lib/cli-gemini.mjs +20 -50
- package/scripts/lib/keyword-rules.mjs +1 -1
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/psmux-info.mjs +50 -5
package/hub/team/native.mjs
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// teammate 프롬프트 템플릿 + 팀 설정 빌더
|
|
3
3
|
//
|
|
4
4
|
// Claude Code 네이티브 Agent Teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)
|
|
5
|
-
// 환경에서 teammate를 Codex/
|
|
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
|
|
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
|
|
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/
|
|
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 "${
|
|
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} "${
|
|
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/
|
|
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
|
}
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -234,7 +234,7 @@ function ensurePsmuxInstalled() {
|
|
|
234
234
|
if (!hasPsmux()) {
|
|
235
235
|
throw new Error(
|
|
236
236
|
"psmux가 설치되어 있지 않습니다.\n\n" +
|
|
237
|
-
"psmux는 Codex/
|
|
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/
|
|
1415
|
+
* CLI 명령(codex/legacy Gemini)이 psmux pane의 PowerShell 환경에서 단축 플래그 충돌을
|
|
1416
1416
|
* 일으키는 문제를 방지하기 위해 bash -c '...' 로 감싼다.
|
|
1417
1417
|
* - codex -o flag → PS -OutVariable/OutBuffer 충돌
|
|
1418
|
-
* -
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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 =
|
|
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 =
|
|
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 });
|
package/hub/team/session.mjs
CHANGED
|
@@ -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'|'
|
|
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 커맨드 실행
|
|
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
|
|
163
|
-
" WSL2: wsl sudo apt install tmux\n" +
|
|
144
|
+
" Windows: psmux 설치\n" +
|
|
164
145
|
" macOS: brew install tmux\n" +
|
|
165
|
-
" Linux: apt install tmux
|
|
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
|
|
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:
|
|
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(
|
|
921
|
-
|
|
922
|
-
|
|
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(() => {
|
|
@@ -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", "
|
|
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 ?? "")
|
package/hub/team/tui-core.mjs
CHANGED
|
@@ -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")
|
package/hub/team/tui-lite.mjs
CHANGED
|
@@ -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}.${
|
|
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)}`,
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -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
|
|
451
|
-
const
|
|
452
|
-
const
|
|
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,
|
|
602
|
+
const displayRole = dedupeRole(role, displayName, cli);
|
|
602
603
|
const title = truncate(
|
|
603
|
-
`${selMark} ${hb} ${hostBadge}${color(
|
|
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: {
|
|
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",
|