@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/headless.mjs
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
19
|
import net from "node:net";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
22
23
|
import { requestJson } from "@triflux/core/hub/bridge.mjs";
|
|
23
24
|
import { escapePwshSingleQuoted } from "@triflux/core/hub/cli-adapter-base.mjs";
|
|
24
25
|
import { getMaxSpawnPerSec } from "@triflux/core/hub/lib/spawn-trace.mjs";
|
|
@@ -28,8 +29,9 @@ import {
|
|
|
28
29
|
buildDaemonExecDispatchPayload,
|
|
29
30
|
deriveClaudeDaemonPaths as deriveClaudeControlPaths,
|
|
30
31
|
killDaemonJob,
|
|
31
|
-
|
|
32
|
+
resolveDaemonBridgeSessionId,
|
|
32
33
|
sendClaudeControlRequest,
|
|
34
|
+
sendKillBySessionId,
|
|
33
35
|
waitForDaemonJobPid,
|
|
34
36
|
} from "./claude-daemon-control.mjs";
|
|
35
37
|
import {
|
|
@@ -67,11 +69,17 @@ import { createWtManager } from "./wt-manager.mjs";
|
|
|
67
69
|
|
|
68
70
|
const RESULT_DIR = join(tmpdir(), "tfx-headless");
|
|
69
71
|
const DAEMON_COMPLETION_PREFIX = "__TFX_HEADLESS_DONE__:";
|
|
72
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
70
73
|
|
|
71
74
|
/** CLI별 브랜드 — 이모지 + 공식 색상 (HUD와 통일) */
|
|
72
75
|
const CLI_BRAND = {
|
|
73
76
|
codex: { emoji: "\u{26AA}", label: "Codex", ansi: "\x1b[97m" }, // ⚪ bright white (codexWhite)
|
|
74
77
|
gemini: { emoji: "\u{1F535}", label: "Gemini", ansi: "\x1b[38;5;39m" }, // 🔵 geminiBlue
|
|
78
|
+
antigravity: {
|
|
79
|
+
emoji: "\u{1F535}",
|
|
80
|
+
label: "Antigravity",
|
|
81
|
+
ansi: "\x1b[38;5;39m",
|
|
82
|
+
},
|
|
75
83
|
claude: {
|
|
76
84
|
emoji: "\u{1F7E0}",
|
|
77
85
|
label: "Claude",
|
|
@@ -84,17 +92,109 @@ const _ANSI_DIM = "\x1b[2m";
|
|
|
84
92
|
/** 에이전트 역할명 → CLI 타입 매핑 (단일 소스: agent-map.json) */
|
|
85
93
|
const _require = createRequire(import.meta.url);
|
|
86
94
|
const AGENT_TO_CLI = _require("./agent-map.json");
|
|
95
|
+
// scripts/tfx-route.sh also validates route agents from agent-map.json.
|
|
96
|
+
const VALID_ROUTE_AGENTS = new Set(Object.keys(AGENT_TO_CLI));
|
|
87
97
|
|
|
88
98
|
/**
|
|
89
|
-
* 에이전트 역할명 또는 CLI 이름을 CLI 타입("codex"|"
|
|
99
|
+
* 에이전트 역할명 또는 CLI 이름을 CLI 타입("codex"|"antigravity"|"claude")으로 해석한다.
|
|
90
100
|
* route_agent()가 적용되지 않는 headless 경로에서 사용.
|
|
91
101
|
* @param {string} agentOrCli — "executor", "codex", "designer" 등
|
|
92
|
-
* @returns {'codex'|'
|
|
102
|
+
* @returns {'codex'|'antigravity'|'claude'} CLI 타입
|
|
93
103
|
*/
|
|
94
104
|
export function resolveCliType(agentOrCli) {
|
|
95
105
|
return AGENT_TO_CLI[agentOrCli] || agentOrCli;
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
function resolveHeadlessCliType(agentOrCli) {
|
|
109
|
+
const resolved = resolveCliType(agentOrCli);
|
|
110
|
+
return resolved === "gemini" ? "antigravity" : resolved;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function shellQuote(value) {
|
|
114
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveHeadlessRouteScript(opts = {}) {
|
|
118
|
+
const candidates = [
|
|
119
|
+
opts.routeScript,
|
|
120
|
+
process.env.TFX_ROUTE_SCRIPT,
|
|
121
|
+
process.env.TFX_DELEGATOR_ROUTE_SCRIPT,
|
|
122
|
+
join(process.cwd(), "scripts", "tfx-route.sh"),
|
|
123
|
+
join(SCRIPT_DIR, "..", "..", "scripts", "tfx-route.sh"),
|
|
124
|
+
process.env.HOME
|
|
125
|
+
? join(process.env.HOME, ".claude", "scripts", "tfx-route.sh")
|
|
126
|
+
: "",
|
|
127
|
+
"tfx-route.sh",
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
if (!candidate) continue;
|
|
132
|
+
if (candidate === "tfx-route.sh" || existsSync(candidate)) {
|
|
133
|
+
return candidate;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return "tfx-route.sh";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveRouteAgentForHeadless(resolvedCli, opts = {}) {
|
|
141
|
+
const role = String(opts.role || "").trim();
|
|
142
|
+
if (
|
|
143
|
+
role &&
|
|
144
|
+
VALID_ROUTE_AGENTS.has(role) &&
|
|
145
|
+
!["gemini", "antigravity", "agy"].includes(role)
|
|
146
|
+
) {
|
|
147
|
+
return role;
|
|
148
|
+
}
|
|
149
|
+
return resolvedCli === "antigravity" ? "antigravity" : resolvedCli;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function normalizeHeadlessRole(role) {
|
|
153
|
+
const normalized = String(role || "")
|
|
154
|
+
.trim()
|
|
155
|
+
.toLowerCase();
|
|
156
|
+
return normalized && VALID_ROUTE_AGENTS.has(normalized) ? normalized : "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function resolveHeadlessDisplayName(assignment = {}, paneName = "") {
|
|
160
|
+
const normalized = String(assignment.displayName || "")
|
|
161
|
+
.trim()
|
|
162
|
+
.replace(/[\r\n\x00-\x1f]/g, " ");
|
|
163
|
+
return normalized || paneName || "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function normalizeHeadlessAssignment(assignment = {}, index = 0) {
|
|
167
|
+
const paneName = `worker-${index + 1}`;
|
|
168
|
+
return {
|
|
169
|
+
...assignment,
|
|
170
|
+
role: normalizeHeadlessRole(assignment.role),
|
|
171
|
+
displayName: resolveHeadlessDisplayName(assignment, paneName),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildRouteBackedHeadlessCommand(
|
|
176
|
+
resolvedCli,
|
|
177
|
+
promptFile,
|
|
178
|
+
resultFile,
|
|
179
|
+
opts = {},
|
|
180
|
+
) {
|
|
181
|
+
const routeScript = resolveHeadlessRouteScript(opts);
|
|
182
|
+
const routeAgent = resolveRouteAgentForHeadless(resolvedCli, opts);
|
|
183
|
+
const mcp = String(opts.mcp || "auto");
|
|
184
|
+
const timeout =
|
|
185
|
+
Number.isFinite(Number(opts.timeoutSec)) && Number(opts.timeoutSec) > 0
|
|
186
|
+
? String(Math.trunc(Number(opts.timeoutSec)))
|
|
187
|
+
: "";
|
|
188
|
+
const timeoutArg = timeout ? ` ${shellQuote(timeout)}` : "";
|
|
189
|
+
const routeMode = resolvedCli === "antigravity" ? "antigravity" : resolvedCli;
|
|
190
|
+
const script =
|
|
191
|
+
`__tfx_prompt=$(cat ${shellQuote(promptFile)}); ` +
|
|
192
|
+
`TFX_CLI_MODE=${shellQuote(routeMode)} ` +
|
|
193
|
+
`bash ${shellQuote(routeScript)} ${shellQuote(routeAgent)} "$__tfx_prompt" ${shellQuote(mcp)}${timeoutArg} ` +
|
|
194
|
+
`> ${shellQuote(resultFile)} 2>${shellQuote(`${resultFile}.err`)} < /dev/null`;
|
|
195
|
+
return `bash -lc ${shellQuote(script)}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
98
198
|
export function getHeadlessWorkerAgentId(sessionName, index) {
|
|
99
199
|
return `headless-${sessionName}-${index}`;
|
|
100
200
|
}
|
|
@@ -173,7 +273,7 @@ const MCP_PROFILE_HINTS = {
|
|
|
173
273
|
|
|
174
274
|
/**
|
|
175
275
|
* CLI별 헤드리스 명령 빌더
|
|
176
|
-
* @param {'codex'|'
|
|
276
|
+
* @param {'codex'|'antigravity'|'claude'} cli
|
|
177
277
|
* @param {string} prompt — 실행할 프롬프트
|
|
178
278
|
* @param {string} resultFile — 결과 저장 파일 경로
|
|
179
279
|
* @param {object} [opts]
|
|
@@ -205,7 +305,7 @@ export function buildDashboardAttachArgs(
|
|
|
205
305
|
|
|
206
306
|
export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
207
307
|
const { handoff = true, mcp, contextFile, model, cwd } = opts;
|
|
208
|
-
const resolvedCli =
|
|
308
|
+
const resolvedCli = resolveHeadlessCliType(cli);
|
|
209
309
|
|
|
210
310
|
// contextFile 처리: 32KB(32768 bytes) 초과 시 UTF-8 안전 절단
|
|
211
311
|
let contextPrefix = "";
|
|
@@ -241,12 +341,17 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
241
341
|
writeFileSync(promptFile, fullPrompt, "utf8");
|
|
242
342
|
void IS_WINDOWS; // referenced for diagnostic guard chain below
|
|
243
343
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
344
|
+
const backendCommand =
|
|
345
|
+
resolvedCli === "antigravity"
|
|
346
|
+
? buildRouteBackedHeadlessCommand(resolvedCli, promptFile, resultFile, {
|
|
347
|
+
...opts,
|
|
348
|
+
mcp,
|
|
349
|
+
})
|
|
350
|
+
: getBackend(resolvedCli).buildArgs(fullPrompt, resultFile, {
|
|
351
|
+
...opts,
|
|
352
|
+
model,
|
|
353
|
+
promptFile,
|
|
354
|
+
});
|
|
250
355
|
const safeCwd =
|
|
251
356
|
typeof cwd === "string" ? cwd.trim().replace(/[\r\n\x00-\x1f]/g, "") : "";
|
|
252
357
|
if (safeCwd && (safeCwd.startsWith("\\\\") || safeCwd.startsWith("//"))) {
|
|
@@ -679,16 +784,15 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
679
784
|
for (let i = 0; i < assignments.length; i++) {
|
|
680
785
|
const assignment = assignments[i];
|
|
681
786
|
const paneName = `worker-${i + 1}`;
|
|
787
|
+
const displayName = resolveHeadlessDisplayName(assignment, paneName);
|
|
682
788
|
const workerId = getHeadlessWorkerAgentId(sessionName, i);
|
|
683
|
-
const resolvedCli =
|
|
789
|
+
const resolvedCli = resolveHeadlessCliType(assignment.cli);
|
|
684
790
|
const brand = CLI_BRAND[resolvedCli] || {
|
|
685
791
|
emoji: "\u{25CF}",
|
|
686
792
|
label: resolvedCli,
|
|
687
793
|
ansi: "",
|
|
688
794
|
};
|
|
689
|
-
const paneTitle =
|
|
690
|
-
? `${brand.emoji} ${resolvedCli} (${assignment.role})`
|
|
691
|
-
: `${brand.emoji} ${resolvedCli}-${i + 1}`;
|
|
795
|
+
const paneTitle = `${brand.emoji} ${resolvedCli} (${displayName})`;
|
|
692
796
|
|
|
693
797
|
let newPaneId;
|
|
694
798
|
// 모든 워커를 split-window로 생성 (lead pane index 0은 비워둠)
|
|
@@ -708,13 +812,14 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
708
812
|
} catch {
|
|
709
813
|
/* 무시 */
|
|
710
814
|
}
|
|
711
|
-
await registerHeadlessWorker(sessionName, i,
|
|
815
|
+
await registerHeadlessWorker(sessionName, i, resolvedCli);
|
|
712
816
|
|
|
713
817
|
if (safeProgress)
|
|
714
818
|
safeProgress({
|
|
715
819
|
type: "worker_added",
|
|
716
820
|
paneName,
|
|
717
|
-
|
|
821
|
+
displayName,
|
|
822
|
+
cli: resolvedCli,
|
|
718
823
|
paneTitle,
|
|
719
824
|
});
|
|
720
825
|
|
|
@@ -731,6 +836,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
731
836
|
resultFile,
|
|
732
837
|
{
|
|
733
838
|
mcp: assignment.mcp,
|
|
839
|
+
role: assignment.role,
|
|
734
840
|
model: assignment.model,
|
|
735
841
|
cwd: assignment.cwd || assignment.workdir,
|
|
736
842
|
},
|
|
@@ -742,14 +848,15 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
742
848
|
registerHeadlessSynapseWorker(workerId, assignment.prompt);
|
|
743
849
|
|
|
744
850
|
if (safeProgress)
|
|
745
|
-
safeProgress({ type: "dispatched", paneName, cli:
|
|
851
|
+
safeProgress({ type: "dispatched", paneName, cli: resolvedCli });
|
|
746
852
|
|
|
747
853
|
dispatches.push({
|
|
748
854
|
...dispatch,
|
|
749
855
|
paneId: newPaneId,
|
|
750
856
|
paneName,
|
|
857
|
+
displayName,
|
|
751
858
|
resultFile,
|
|
752
|
-
cli:
|
|
859
|
+
cli: resolvedCli,
|
|
753
860
|
role: assignment.role,
|
|
754
861
|
command: cmd,
|
|
755
862
|
workerId,
|
|
@@ -796,7 +903,9 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
796
903
|
return await Promise.all(
|
|
797
904
|
assignments.map(async (assignment, i) => {
|
|
798
905
|
const paneName = `worker-${i + 1}`;
|
|
906
|
+
const displayName = resolveHeadlessDisplayName(assignment, paneName);
|
|
799
907
|
const workerId = getHeadlessWorkerAgentId(sessionName, i);
|
|
908
|
+
const resolvedCli = resolveHeadlessCliType(assignment.cli);
|
|
800
909
|
const resultFile = join(
|
|
801
910
|
RESULT_DIR,
|
|
802
911
|
`${sessionName}-${paneName}.txt`,
|
|
@@ -809,12 +918,13 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
809
918
|
resultFile,
|
|
810
919
|
{
|
|
811
920
|
mcp: assignment.mcp,
|
|
921
|
+
role: assignment.role,
|
|
812
922
|
model: assignment.model,
|
|
813
923
|
cwd: assignment.cwd || assignment.workdir,
|
|
814
924
|
},
|
|
815
925
|
);
|
|
816
926
|
const scriptDir = join(RESULT_DIR, sessionName);
|
|
817
|
-
await registerHeadlessWorker(sessionName, i,
|
|
927
|
+
await registerHeadlessWorker(sessionName, i, resolvedCli);
|
|
818
928
|
const dispatch = dispatchCommand(sessionName, paneName, cmd, {
|
|
819
929
|
scriptDir,
|
|
820
930
|
scriptName: paneName,
|
|
@@ -826,13 +936,19 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
826
936
|
// progressive 모드에서는 split-window 시 새 pane에 바로 타이틀이 설정되므로 문제없음
|
|
827
937
|
|
|
828
938
|
if (safeProgress)
|
|
829
|
-
safeProgress({
|
|
939
|
+
safeProgress({
|
|
940
|
+
type: "dispatched",
|
|
941
|
+
paneName,
|
|
942
|
+
displayName,
|
|
943
|
+
cli: resolvedCli,
|
|
944
|
+
});
|
|
830
945
|
|
|
831
946
|
return {
|
|
832
947
|
...dispatch,
|
|
833
948
|
paneName,
|
|
949
|
+
displayName,
|
|
834
950
|
resultFile,
|
|
835
|
-
cli:
|
|
951
|
+
cli: resolvedCli,
|
|
836
952
|
role: assignment.role,
|
|
837
953
|
command: cmd,
|
|
838
954
|
workerId,
|
|
@@ -851,7 +967,9 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
|
851
967
|
for (let i = 0; i < assignments.length; i += 1) {
|
|
852
968
|
const assignment = assignments[i];
|
|
853
969
|
const paneName = `worker-${i + 1}`;
|
|
970
|
+
const displayName = resolveHeadlessDisplayName(assignment, paneName);
|
|
854
971
|
const workerId = getHeadlessWorkerAgentId(sessionName, i);
|
|
972
|
+
const resolvedCli = resolveHeadlessCliType(assignment.cli);
|
|
855
973
|
const resultFile = join(
|
|
856
974
|
RESULT_DIR,
|
|
857
975
|
`${sessionName}-${paneName}.txt`,
|
|
@@ -863,18 +981,20 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
|
863
981
|
resultFile,
|
|
864
982
|
{
|
|
865
983
|
mcp: assignment.mcp,
|
|
984
|
+
role: assignment.role,
|
|
866
985
|
model: assignment.model,
|
|
867
986
|
cwd: assignment.cwd || assignment.workdir,
|
|
868
987
|
},
|
|
869
988
|
);
|
|
870
989
|
const token = randomUUID().slice(0, 10);
|
|
871
990
|
const short = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
872
|
-
const name = `Triflux ${
|
|
991
|
+
const name = `Triflux ${resolvedCli || "worker"} ${displayName}`;
|
|
873
992
|
const record = {
|
|
874
993
|
paneId: `daemon:${short}`,
|
|
875
994
|
paneName,
|
|
995
|
+
displayName,
|
|
876
996
|
resultFile,
|
|
877
|
-
cli:
|
|
997
|
+
cli: resolvedCli,
|
|
878
998
|
role: assignment.role,
|
|
879
999
|
command,
|
|
880
1000
|
token,
|
|
@@ -909,6 +1029,11 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
|
909
1029
|
}
|
|
910
1030
|
|
|
911
1031
|
const job = await waitForDaemonJobPid(paths.controlSock, short);
|
|
1032
|
+
const bridgeSessionId = await resolveDaemonBridgeSessionId({
|
|
1033
|
+
daemonPaths: paths,
|
|
1034
|
+
short,
|
|
1035
|
+
job,
|
|
1036
|
+
});
|
|
912
1037
|
const projection = buildClaudeSessionProjection({
|
|
913
1038
|
pid: job.pid,
|
|
914
1039
|
procStart: getProcStart(job.pid),
|
|
@@ -916,18 +1041,19 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
|
916
1041
|
short,
|
|
917
1042
|
cwd: assignment.cwd || assignment.workdir || process.cwd(),
|
|
918
1043
|
name,
|
|
919
|
-
agent:
|
|
1044
|
+
agent: resolvedCli || "codex",
|
|
920
1045
|
startedAt: job.startedAt || Date.now(),
|
|
921
1046
|
updatedAt: Date.now(),
|
|
1047
|
+
bridgeSessionId,
|
|
922
1048
|
});
|
|
923
1049
|
record.sessionProjectionPath = await writeClaudeSessionProjection(
|
|
924
1050
|
paths.sessionsDir,
|
|
925
1051
|
projection,
|
|
926
1052
|
);
|
|
927
|
-
await registerHeadlessWorker(sessionName, i,
|
|
1053
|
+
await registerHeadlessWorker(sessionName, i, resolvedCli);
|
|
928
1054
|
registerHeadlessSynapseWorker(workerId, assignment.prompt);
|
|
929
1055
|
if (safeProgress) {
|
|
930
|
-
safeProgress({ type: "dispatched", paneName, cli:
|
|
1056
|
+
safeProgress({ type: "dispatched", paneName, cli: resolvedCli });
|
|
931
1057
|
}
|
|
932
1058
|
}
|
|
933
1059
|
} catch (error) {
|
|
@@ -997,6 +1123,7 @@ async function awaitAll(
|
|
|
997
1123
|
safeProgress({
|
|
998
1124
|
type: "progress",
|
|
999
1125
|
paneName: d.paneName,
|
|
1126
|
+
displayName: d.displayName,
|
|
1000
1127
|
cli: d.cli,
|
|
1001
1128
|
snapshot: content.split("\n").slice(-15).join("\n"), // 마지막 15줄
|
|
1002
1129
|
});
|
|
@@ -1015,6 +1142,7 @@ async function awaitAll(
|
|
|
1015
1142
|
safeProgress({
|
|
1016
1143
|
type: "progress",
|
|
1017
1144
|
paneName: d.paneName,
|
|
1145
|
+
displayName: d.displayName,
|
|
1018
1146
|
cli: d.cli,
|
|
1019
1147
|
snapshot: snapshot.split("\n").slice(-15).join("\n"),
|
|
1020
1148
|
});
|
|
@@ -1092,6 +1220,7 @@ async function awaitAll(
|
|
|
1092
1220
|
safeProgress({
|
|
1093
1221
|
type: "completed",
|
|
1094
1222
|
paneName: d.paneName,
|
|
1223
|
+
displayName: d.displayName,
|
|
1095
1224
|
cli: d.cli,
|
|
1096
1225
|
matched: completion.matched,
|
|
1097
1226
|
exitCode: completion.exitCode,
|
|
@@ -1184,6 +1313,7 @@ async function waitForDaemonCompletion(
|
|
|
1184
1313
|
safeProgress({
|
|
1185
1314
|
type: "progress",
|
|
1186
1315
|
paneName: dispatch.paneName,
|
|
1316
|
+
displayName: dispatch.displayName,
|
|
1187
1317
|
cli: dispatch.cli,
|
|
1188
1318
|
snapshot: message.line.split("\n").slice(-15).join("\n"),
|
|
1189
1319
|
});
|
|
@@ -1220,6 +1350,7 @@ async function awaitAllDaemon(
|
|
|
1220
1350
|
safeProgress({
|
|
1221
1351
|
type: "completed",
|
|
1222
1352
|
paneName: d.paneName,
|
|
1353
|
+
displayName: d.displayName,
|
|
1223
1354
|
cli: d.cli,
|
|
1224
1355
|
matched: completion.matched,
|
|
1225
1356
|
exitCode: completion.exitCode,
|
|
@@ -1276,6 +1407,7 @@ async function collectResults(sessionName, results) {
|
|
|
1276
1407
|
return {
|
|
1277
1408
|
cli: d.cli,
|
|
1278
1409
|
paneName: d.paneName,
|
|
1410
|
+
displayName: d.displayName,
|
|
1279
1411
|
paneId: d.paneId,
|
|
1280
1412
|
workerId: d.workerId,
|
|
1281
1413
|
role: d.role,
|
|
@@ -1337,8 +1469,11 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1337
1469
|
} = opts;
|
|
1338
1470
|
|
|
1339
1471
|
mkdirSync(RESULT_DIR, { recursive: true });
|
|
1472
|
+
const normalizedAssignments = (assignments || []).map((assignment, index) =>
|
|
1473
|
+
normalizeHeadlessAssignment(assignment, index),
|
|
1474
|
+
);
|
|
1340
1475
|
|
|
1341
|
-
if (
|
|
1476
|
+
if (normalizedAssignments.length === 0) {
|
|
1342
1477
|
return { sessionName, results: [] };
|
|
1343
1478
|
}
|
|
1344
1479
|
|
|
@@ -1349,11 +1484,16 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1349
1484
|
"[headless] --native-bridge-mode claude-wrapper is reserved and not implemented yet",
|
|
1350
1485
|
);
|
|
1351
1486
|
}
|
|
1352
|
-
if (
|
|
1487
|
+
if (
|
|
1488
|
+
nativeBridge &&
|
|
1489
|
+
(nativeBridgeMode === "roster" || nativeBridgeMode === "interactive-attach")
|
|
1490
|
+
) {
|
|
1353
1491
|
nativeBridgeHandle = await startClaudeNativeBridge({
|
|
1354
1492
|
sessionName,
|
|
1355
|
-
assignments,
|
|
1493
|
+
assignments: normalizedAssignments,
|
|
1356
1494
|
cwd: process.cwd(),
|
|
1495
|
+
workerType:
|
|
1496
|
+
nativeBridgeMode === "interactive-attach" ? "interactive" : "headless",
|
|
1357
1497
|
onKill() {
|
|
1358
1498
|
try {
|
|
1359
1499
|
killPsmuxSession(sessionName);
|
|
@@ -1381,11 +1521,11 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1381
1521
|
.catch(() => {});
|
|
1382
1522
|
|
|
1383
1523
|
// Synapse: 세션 registration (fire-and-forget, hub 미응답 시 무시)
|
|
1384
|
-
const synapseIds =
|
|
1524
|
+
const synapseIds = normalizedAssignments.map(
|
|
1385
1525
|
(_, i) => `${sessionName}-worker-${i + 1}`,
|
|
1386
1526
|
);
|
|
1387
|
-
for (let i = 0; i <
|
|
1388
|
-
const a =
|
|
1527
|
+
for (let i = 0; i < normalizedAssignments.length; i++) {
|
|
1528
|
+
const a = normalizedAssignments[i];
|
|
1389
1529
|
requestJson("/synapse/register", {
|
|
1390
1530
|
method: "POST",
|
|
1391
1531
|
body: {
|
|
@@ -1402,7 +1542,7 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1402
1542
|
let tui = null;
|
|
1403
1543
|
const resolvedLayout = resolveDashboardLayout(
|
|
1404
1544
|
dashboardLayout,
|
|
1405
|
-
|
|
1545
|
+
normalizedAssignments.length,
|
|
1406
1546
|
);
|
|
1407
1547
|
if (dashboard && process.stdout.isTTY) {
|
|
1408
1548
|
tui = createLogDashboard({
|
|
@@ -1413,10 +1553,12 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1413
1553
|
});
|
|
1414
1554
|
tui.setStartTime(Date.now());
|
|
1415
1555
|
// 초기 워커 상태 등록
|
|
1416
|
-
for (let i = 0; i <
|
|
1417
|
-
const a =
|
|
1418
|
-
|
|
1419
|
-
|
|
1556
|
+
for (let i = 0; i < normalizedAssignments.length; i++) {
|
|
1557
|
+
const a = normalizedAssignments[i];
|
|
1558
|
+
const paneName = `worker-${i + 1}`;
|
|
1559
|
+
tui.updateWorker(paneName, {
|
|
1560
|
+
cli: resolveHeadlessCliType(a.cli || "codex"),
|
|
1561
|
+
displayName: resolveHeadlessDisplayName(a, paneName),
|
|
1420
1562
|
role: a.role || "",
|
|
1421
1563
|
status: "pending",
|
|
1422
1564
|
progress: 0,
|
|
@@ -1437,12 +1579,14 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1437
1579
|
// per-worker state feed: onProgress 이벤트 → tui.updateWorker()
|
|
1438
1580
|
function feedTui(event) {
|
|
1439
1581
|
if (!tui) return;
|
|
1440
|
-
const { type, paneName, cli, snapshot, matched, exitCode } =
|
|
1582
|
+
const { type, paneName, displayName, cli, snapshot, matched, exitCode } =
|
|
1583
|
+
event;
|
|
1441
1584
|
if (!paneName) return;
|
|
1442
1585
|
|
|
1443
1586
|
if (type === "progress" && snapshot) {
|
|
1444
1587
|
tui.updateWorker(paneName, {
|
|
1445
1588
|
cli: cli || "codex",
|
|
1589
|
+
displayName,
|
|
1446
1590
|
status: "running",
|
|
1447
1591
|
snapshot: snapshot.split("\n").at(-1) || "",
|
|
1448
1592
|
summary: snapshot.split("\n").at(-1) || "",
|
|
@@ -1453,12 +1597,14 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1453
1597
|
const status = matched && exitCode === 0 ? "completed" : "failed";
|
|
1454
1598
|
tui.updateWorker(paneName, {
|
|
1455
1599
|
cli: cli || "codex",
|
|
1600
|
+
displayName,
|
|
1456
1601
|
status,
|
|
1457
1602
|
progress: 1,
|
|
1458
1603
|
});
|
|
1459
1604
|
} else if (type === "worker_added") {
|
|
1460
1605
|
tui.updateWorker(paneName, {
|
|
1461
1606
|
cli: cli || "codex",
|
|
1607
|
+
displayName,
|
|
1462
1608
|
status: "running",
|
|
1463
1609
|
progress: 0.05,
|
|
1464
1610
|
});
|
|
@@ -1507,16 +1653,16 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1507
1653
|
try {
|
|
1508
1654
|
const dispatches =
|
|
1509
1655
|
nativeBridge && nativeBridgeMode === "agents"
|
|
1510
|
-
? await dispatchDaemonBatch(sessionName,
|
|
1656
|
+
? await dispatchDaemonBatch(sessionName, normalizedAssignments, {
|
|
1511
1657
|
safeProgress,
|
|
1512
1658
|
})
|
|
1513
1659
|
: progressive
|
|
1514
|
-
? await dispatchProgressive(sessionName,
|
|
1660
|
+
? await dispatchProgressive(sessionName, normalizedAssignments, {
|
|
1515
1661
|
layout,
|
|
1516
1662
|
safeProgress,
|
|
1517
1663
|
dashboardLayout,
|
|
1518
1664
|
})
|
|
1519
|
-
: await dispatchBatch(sessionName,
|
|
1665
|
+
: await dispatchBatch(sessionName, normalizedAssignments, {
|
|
1520
1666
|
layout,
|
|
1521
1667
|
safeProgress,
|
|
1522
1668
|
dashboardLayout,
|
|
@@ -1549,6 +1695,7 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1549
1695
|
for (const r of collected) {
|
|
1550
1696
|
tui.updateWorker(r.paneName, {
|
|
1551
1697
|
cli: r.cli,
|
|
1698
|
+
displayName: r.displayName,
|
|
1552
1699
|
role: r.role || "",
|
|
1553
1700
|
status: r.handoff?.status === "failed" ? "failed" : "completed",
|
|
1554
1701
|
handoff: r.handoff,
|
|
@@ -1950,6 +2097,7 @@ export async function runHeadlessInteractive(
|
|
|
1950
2097
|
// Fix P2: paneId를 dispatches에 포함 (snapshots에서 필요)
|
|
1951
2098
|
const dispatches = results.map((r, i) => ({
|
|
1952
2099
|
paneName: r.paneName,
|
|
2100
|
+
displayName: r.displayName,
|
|
1953
2101
|
paneId: r.paneId || "",
|
|
1954
2102
|
cli: r.cli,
|
|
1955
2103
|
role: r.role,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildNativeWorkerRosterEntry,
|
|
7
|
+
deriveClaudeDaemonPaths,
|
|
8
|
+
mergeRosterWorkers,
|
|
9
|
+
removeRosterWorkers,
|
|
10
|
+
} from "./claude-native-bridge.mjs";
|
|
11
|
+
import { startInteractiveNativeWorker } from "./interactive-native-worker.mjs";
|
|
12
|
+
|
|
13
|
+
function buildWorkerSocketPaths({ configDir, sessionName, pid, short }) {
|
|
14
|
+
const hash = crypto
|
|
15
|
+
.createHash("sha256")
|
|
16
|
+
.update(`${configDir || ""}:${sessionName}:${pid}:${crypto.randomUUID()}`)
|
|
17
|
+
.digest("hex")
|
|
18
|
+
.slice(0, 10);
|
|
19
|
+
const socketRoot = path.join(os.tmpdir(), `tfx-in-${hash}`);
|
|
20
|
+
return {
|
|
21
|
+
rvSock: path.join(socketRoot, `${short}.rv.sock`),
|
|
22
|
+
ptySock: path.join(socketRoot, `${short}.pty.sock`),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function launchAdoptedInteractiveWorker({
|
|
27
|
+
cwd = process.cwd(),
|
|
28
|
+
sessionName,
|
|
29
|
+
launchCmd = "codex",
|
|
30
|
+
cli = "codex",
|
|
31
|
+
role = "interactive",
|
|
32
|
+
configDir,
|
|
33
|
+
pid = process.pid,
|
|
34
|
+
_deps = {},
|
|
35
|
+
} = {}) {
|
|
36
|
+
const startWorker =
|
|
37
|
+
_deps.startInteractiveNativeWorker || startInteractiveNativeWorker;
|
|
38
|
+
const buildRosterEntry =
|
|
39
|
+
_deps.buildNativeWorkerRosterEntry || buildNativeWorkerRosterEntry;
|
|
40
|
+
const mergeWorkers = _deps.mergeRosterWorkers || mergeRosterWorkers;
|
|
41
|
+
const derivePaths = _deps.deriveClaudeDaemonPaths || deriveClaudeDaemonPaths;
|
|
42
|
+
|
|
43
|
+
const resolvedCwd = path.resolve(cwd);
|
|
44
|
+
const effectiveSessionName =
|
|
45
|
+
sessionName || `tfx-${cli}-${role}-${process.pid}`;
|
|
46
|
+
const paths = derivePaths({ configDir });
|
|
47
|
+
const short = crypto.randomUUID().replaceAll("-", "").slice(0, 8);
|
|
48
|
+
const { rvSock, ptySock } = buildWorkerSocketPaths({
|
|
49
|
+
configDir,
|
|
50
|
+
sessionName: effectiveSessionName,
|
|
51
|
+
pid,
|
|
52
|
+
short,
|
|
53
|
+
});
|
|
54
|
+
const displayName = `Triflux ${cli} ${role}`;
|
|
55
|
+
|
|
56
|
+
let worker;
|
|
57
|
+
let closed = false;
|
|
58
|
+
let closePromise = null;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
worker = await startWorker({
|
|
62
|
+
short,
|
|
63
|
+
rvSock,
|
|
64
|
+
ptySock,
|
|
65
|
+
sessionName: effectiveSessionName,
|
|
66
|
+
cwd: resolvedCwd,
|
|
67
|
+
launchCmd,
|
|
68
|
+
pid,
|
|
69
|
+
});
|
|
70
|
+
const workerShort = worker.short || short;
|
|
71
|
+
const workerPid = worker.pid || pid;
|
|
72
|
+
const entry = buildRosterEntry({
|
|
73
|
+
short: workerShort,
|
|
74
|
+
pid: workerPid,
|
|
75
|
+
cwd: resolvedCwd,
|
|
76
|
+
rendezvousSock: worker.rvSock,
|
|
77
|
+
ptySock: worker.ptySock,
|
|
78
|
+
name: displayName,
|
|
79
|
+
prompt: displayName,
|
|
80
|
+
});
|
|
81
|
+
await mergeWorkers(paths.rosterPath, { [workerShort]: entry });
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
short: workerShort,
|
|
85
|
+
displayName,
|
|
86
|
+
rosterPath: paths.rosterPath,
|
|
87
|
+
close() {
|
|
88
|
+
if (!closePromise) {
|
|
89
|
+
closePromise = (async () => {
|
|
90
|
+
if (closed) return;
|
|
91
|
+
closed = true;
|
|
92
|
+
try {
|
|
93
|
+
await worker.close();
|
|
94
|
+
} finally {
|
|
95
|
+
await removeRosterWorkers(paths.rosterPath, [workerShort]);
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
}
|
|
99
|
+
return closePromise;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (worker) await worker.close().catch(() => {});
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|