@triflux/remote 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.
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 +244 -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 +63 -14
  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
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import {
8
8
  buildDaemonExecDispatchPayload,
9
9
  killDaemonJob,
10
+ resolveDaemonBridgeSessionId,
10
11
  sendClaudeControlRequest,
11
12
  waitForDaemonJobPid,
12
13
  } from "./claude-daemon-control.mjs";
@@ -15,6 +16,7 @@ import {
15
16
  removeClaudeSessionProjection,
16
17
  writeClaudeSessionProjection,
17
18
  } from "./claude-session-projection.mjs";
19
+ import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
18
20
 
19
21
  const DEFAULT_ROWS = 40;
20
22
  const DEFAULT_COLS = 120;
@@ -230,6 +232,30 @@ function buildSwarmShardBridgeCommand({ displayName, sessionId }) {
230
232
  return `printf '%s\\n' ${shellSingleQuote(banner)}; while true; do sleep 3600; done`;
231
233
  }
232
234
 
235
+ function normalizeWorkerType(value) {
236
+ return value === "interactive" ? "interactive" : "headless";
237
+ }
238
+
239
+ function buildInteractiveTransportSessionName(...parts) {
240
+ const raw = parts.filter(Boolean).join("-");
241
+ return raw
242
+ .replace(/[^A-Za-z0-9_.-]+/g, "-")
243
+ .replace(/^-+|-+$/g, "")
244
+ .slice(0, 80);
245
+ }
246
+
247
+ function createStopOnce(transport) {
248
+ let stopPromise = null;
249
+ return () => {
250
+ if (!stopPromise) {
251
+ stopPromise = Promise.resolve()
252
+ .then(() => transport.stop())
253
+ .catch(() => {});
254
+ }
255
+ return stopPromise;
256
+ };
257
+ }
258
+
233
259
  function compactDisplayPart(value, fallback = "") {
234
260
  const text = String(value ?? "")
235
261
  .replace(/[\u0000-\u001f\u007f]+/g, " ")
@@ -296,6 +322,10 @@ export async function registerSwarmShard({
296
322
  shardName,
297
323
  cwd = process.cwd(),
298
324
  host = "local",
325
+ interactive = false,
326
+ workerType,
327
+ launchCmd = "codex",
328
+ env = {},
299
329
  configDir = resolveClaudeConfigDir(),
300
330
  tmpRoot = "/tmp",
301
331
  _deps = {},
@@ -333,6 +363,8 @@ export async function registerSwarmShard({
333
363
  const sendControl =
334
364
  _deps.sendClaudeControlRequest || sendClaudeControlRequest;
335
365
  const waitForPid = _deps.waitForDaemonJobPid || waitForDaemonJobPid;
366
+ const resolveBridgeSessionId =
367
+ _deps.resolveDaemonBridgeSessionId || resolveDaemonBridgeSessionId;
336
368
  const readProcStart = _deps.getProcStart || getProcStart;
337
369
  const buildProjection =
338
370
  _deps.buildClaudeSessionProjection || buildClaudeSessionProjection;
@@ -340,6 +372,10 @@ export async function registerSwarmShard({
340
372
  _deps.writeClaudeSessionProjection || writeClaudeSessionProjection;
341
373
  const removeProjection =
342
374
  _deps.removeClaudeSessionProjection || removeClaudeSessionProjection;
375
+ const mergeRoster = _deps.mergeRosterWorkers || mergeRosterWorkers;
376
+ const removeRoster = _deps.removeRosterWorkers || removeRosterWorkers;
377
+ const createTransport =
378
+ _deps.createInteractiveTuiTransport || createInteractiveTuiTransport;
343
379
  const removeJobStateImpl = _deps.removeClaudeJobState || removeClaudeJobState;
344
380
  const killJob = _deps.killDaemonJob || killDaemonJob;
345
381
  const accessControlSock = _deps.accessControlSock || fs.access;
@@ -347,9 +383,103 @@ export async function registerSwarmShard({
347
383
  const paths = derivePaths({ configDir, tmpRoot });
348
384
  const sessionsDir =
349
385
  paths.sessionsDir || path.join(paths.configDir || configDir, "sessions");
350
- const jobsDir = paths.jobsDir || path.join(paths.configDir || configDir, "jobs");
386
+ const jobsDir =
387
+ paths.jobsDir || path.join(paths.configDir || configDir, "jobs");
351
388
  const short = deriveSwarmShort({ sessionId, shardName, host });
352
389
  const command = buildSwarmShardBridgeCommand({ displayName, sessionId });
390
+ const resolvedWorkerType = interactive
391
+ ? "interactive"
392
+ : normalizeWorkerType(workerType);
393
+
394
+ if (resolvedWorkerType === "interactive") {
395
+ const rvSock = path.join(paths.rendezvousDir, `${short}.rv.sock`);
396
+ const ptySock = path.join(paths.ptyDir, `${short}.pty.sock`);
397
+ let facade = null;
398
+ let rosterWritten = false;
399
+ const transport = createTransport({
400
+ sessionName: buildInteractiveTransportSessionName(
401
+ "tfx",
402
+ "swarm",
403
+ sessionId,
404
+ shardName,
405
+ short,
406
+ ),
407
+ cwd,
408
+ launchCmd,
409
+ env,
410
+ onData(chunk) {
411
+ if (facade) facade.writeOutput(chunk);
412
+ },
413
+ });
414
+ const stopTransport = createStopOnce(transport);
415
+
416
+ try {
417
+ facade = await startNativeWorkerFacade({
418
+ short,
419
+ rvSock,
420
+ ptySock,
421
+ pid: process.pid,
422
+ initialDetail: `${cli}:${role}:interactive`,
423
+ workerType: "interactive",
424
+ onInput(payload) {
425
+ void Promise.resolve()
426
+ .then(() => transport.writeInput(payload))
427
+ .catch(() => {});
428
+ },
429
+ onResize(size) {
430
+ void Promise.resolve()
431
+ .then(() => transport.resize(size))
432
+ .catch(() => {});
433
+ },
434
+ onKill() {
435
+ void stopTransport();
436
+ },
437
+ });
438
+ await transport.start();
439
+ const rosterEntry = buildNativeWorkerRosterEntry({
440
+ short,
441
+ pid: process.pid,
442
+ procStart: readProcStart(process.pid),
443
+ sessionId,
444
+ cwd,
445
+ startedAt: Date.now(),
446
+ rendezvousSock: rvSock,
447
+ ptySock,
448
+ name: displayName,
449
+ prompt: displayName,
450
+ });
451
+ await mergeRoster(paths.rosterPath, { [short]: rosterEntry });
452
+ rosterWritten = true;
453
+ } catch (error) {
454
+ await stopTransport();
455
+ if (facade) await facade.close({ exitCode: 1 }).catch(() => {});
456
+ throw error;
457
+ }
458
+
459
+ let closed = false;
460
+ return {
461
+ ok: true,
462
+ skipped: false,
463
+ interactive: true,
464
+ host: "local",
465
+ sessionId,
466
+ swarmSessionId: sessionId,
467
+ shardName,
468
+ cli,
469
+ role,
470
+ short,
471
+ displayName,
472
+ rosterPath: paths.rosterPath,
473
+ async close() {
474
+ if (closed) return;
475
+ closed = true;
476
+ if (rosterWritten) await removeRoster(paths.rosterPath, [short]);
477
+ await stopTransport();
478
+ if (facade) await facade.close().catch(() => {});
479
+ },
480
+ };
481
+ }
482
+
353
483
  const payload = buildPayload({
354
484
  short,
355
485
  cwd,
@@ -376,6 +506,12 @@ export async function registerSwarmShard({
376
506
 
377
507
  const job = await waitForPid(paths.controlSock, short, { timeoutMs: 1000 });
378
508
  const pid = job.pid;
509
+ const bridgeSessionId = await resolveBridgeSessionId({
510
+ daemonPaths: paths,
511
+ short,
512
+ job,
513
+ timeoutMs: 1000,
514
+ });
379
515
  const projection = buildProjection({
380
516
  pid,
381
517
  procStart: readProcStart(pid),
@@ -386,6 +522,7 @@ export async function registerSwarmShard({
386
522
  agent: cli,
387
523
  startedAt: job.startedAt || Date.now(),
388
524
  updatedAt: Date.now(),
525
+ bridgeSessionId,
389
526
  });
390
527
  const sessionProjectionPath = await writeProjection(sessionsDir, projection);
391
528
  let closed = false;
@@ -469,6 +606,9 @@ export async function startNativeWorkerFacade({
469
606
  pid = process.pid,
470
607
  version = "triflux-native-bridge",
471
608
  initialDetail = "pending",
609
+ workerType = "headless",
610
+ onInput,
611
+ onResize,
472
612
  onKill,
473
613
  } = {}) {
474
614
  if (!short) throw new Error("short is required");
@@ -505,12 +645,19 @@ export async function startNativeWorkerFacade({
505
645
  transcript = trimTranscript(Buffer.concat([transcript, payload]));
506
646
  broadcastPty(buildPtyDataFrame(payload));
507
647
  };
648
+ const handlePtyInput =
649
+ workerType === "interactive" && typeof onInput === "function"
650
+ ? onInput
651
+ : appendOutput;
508
652
 
509
653
  const rvServer = net.createServer((socket) => {
510
654
  rvSockets.add(socket);
511
655
  events.push({ type: "rv-connect" });
512
656
  socket.setEncoding("utf8");
513
657
  socket.once("close", () => rvSockets.delete(socket));
658
+ socket.on("error", (error) => {
659
+ events.push({ type: "rv-socket-error", message: error.message });
660
+ });
514
661
  writeJsonLine(socket, { type: "heartbeat" });
515
662
  writeJsonLine(socket, { type: "state", patch: state });
516
663
  socket.on("data", (chunk) => {
@@ -546,6 +693,9 @@ export async function startNativeWorkerFacade({
546
693
  ptySockets.add(socket);
547
694
  events.push({ type: "pty-connect" });
548
695
  socket.once("close", () => ptySockets.delete(socket));
696
+ socket.on("error", (error) => {
697
+ events.push({ type: "pty-socket-error", message: error.message });
698
+ });
549
699
  socket.write(buildPtyControlFrame({ t: "hello", replPid: pid, version }));
550
700
  if (transcript.length > 0) socket.write(buildPtyDataFrame(transcript));
551
701
  socket.write(buildPtyControlFrame({ t: "live" }));
@@ -562,8 +712,25 @@ export async function startNativeWorkerFacade({
562
712
  ? frame.ctrl
563
713
  : { kind: 0, bytes: frame.payload.length },
564
714
  });
565
- if (frame.kind === 0) appendOutput(frame.payload);
715
+ if (frame.kind === 0) handlePtyInput(frame.payload);
566
716
  else if (frame.ctrl.t === "resize") {
717
+ if (
718
+ workerType === "interactive" &&
719
+ typeof onResize === "function"
720
+ ) {
721
+ const cols = toPositiveInteger(frame.ctrl.cols);
722
+ const rows = toPositiveInteger(frame.ctrl.rows);
723
+ if (cols && rows) {
724
+ void Promise.resolve()
725
+ .then(() => onResize({ cols, rows }))
726
+ .catch((error) => {
727
+ events.push({
728
+ type: "pty-resize-error",
729
+ message: error.message,
730
+ });
731
+ });
732
+ }
733
+ }
567
734
  socket.write(buildPtyControlFrame({ t: "live" }));
568
735
  } else if (frame.ctrl.t === "kill") {
569
736
  socket.write(
@@ -633,6 +800,10 @@ export async function startClaudeNativeBridge({
633
800
  procStart,
634
801
  tmpRoot = os.tmpdir(),
635
802
  onKill,
803
+ workerType = "headless",
804
+ launchCmd = "codex",
805
+ env = {},
806
+ createTransport = createInteractiveTuiTransport,
636
807
  } = {}) {
637
808
  if (!sessionName) throw new Error("sessionName is required");
638
809
  const paths = deriveClaudeDaemonPaths({ configDir, tmpRoot: "/tmp" });
@@ -647,6 +818,7 @@ export async function startClaudeNativeBridge({
647
818
  const startedAt = Date.now();
648
819
  const facades = [];
649
820
  const sentinels = [];
821
+ const transportStops = [];
650
822
  const rosterWorkers = {};
651
823
  const byPaneName = new Map();
652
824
 
@@ -675,15 +847,66 @@ export async function startClaudeNativeBridge({
675
847
  if (!workerPid) throw new Error("native bridge sentinel pid unavailable");
676
848
  const rvSock = path.join(socketRoot, `${short}.rv.sock`);
677
849
  const ptySock = path.join(socketRoot, `${short}.pty.sock`);
678
- const facade = await startNativeWorkerFacade({
679
- short,
680
- rvSock,
681
- ptySock,
682
- pid: workerPid,
683
- initialDetail: `${cli}:${role}:pending`,
684
- onKill,
685
- });
686
850
  const workerCwd = assignment.cwd || assignment.workdir || cwd;
851
+ const assignmentWorkerType = normalizeWorkerType(
852
+ assignment.workerType || workerType,
853
+ );
854
+ let facade = null;
855
+ if (assignmentWorkerType === "interactive") {
856
+ const transport = createTransport({
857
+ sessionName: buildInteractiveTransportSessionName(
858
+ sessionName,
859
+ paneName,
860
+ short,
861
+ ),
862
+ cwd: workerCwd,
863
+ launchCmd: assignment.launchCmd || launchCmd,
864
+ env: assignment.env || env,
865
+ onData(chunk) {
866
+ if (facade) facade.writeOutput(chunk);
867
+ },
868
+ });
869
+ const stopTransport = createStopOnce(transport);
870
+ transportStops.push(stopTransport);
871
+ facade = await startNativeWorkerFacade({
872
+ short,
873
+ rvSock,
874
+ ptySock,
875
+ pid: workerPid,
876
+ initialDetail: `${cli}:${role}:interactive`,
877
+ workerType: "interactive",
878
+ onInput(payload) {
879
+ void Promise.resolve()
880
+ .then(() => transport.writeInput(payload))
881
+ .catch(() => {});
882
+ },
883
+ onResize(size) {
884
+ void Promise.resolve()
885
+ .then(() => transport.resize(size))
886
+ .catch(() => {});
887
+ },
888
+ onKill(event) {
889
+ void stopTransport();
890
+ if (typeof onKill === "function") onKill(event);
891
+ },
892
+ });
893
+ try {
894
+ await transport.start();
895
+ } catch (error) {
896
+ await stopTransport();
897
+ await facade.close({ exitCode: 1 }).catch(() => {});
898
+ throw error;
899
+ }
900
+ } else {
901
+ facade = await startNativeWorkerFacade({
902
+ short,
903
+ rvSock,
904
+ ptySock,
905
+ pid: workerPid,
906
+ initialDetail: `${cli}:${role}:pending`,
907
+ onKill,
908
+ });
909
+ }
687
910
  rosterWorkers[short] = buildNativeWorkerRosterEntry({
688
911
  short,
689
912
  pid: workerPid,
@@ -696,11 +919,18 @@ export async function startClaudeNativeBridge({
696
919
  prompt: assignment.prompt || "",
697
920
  });
698
921
  facades.push(facade);
699
- byPaneName.set(paneName, { facade, cli, role, short });
922
+ byPaneName.set(paneName, {
923
+ facade,
924
+ cli,
925
+ role,
926
+ short,
927
+ workerType: assignmentWorkerType,
928
+ });
700
929
  }
701
930
 
702
931
  await mergeRosterWorkers(paths.rosterPath, rosterWorkers);
703
932
  } catch (error) {
933
+ await Promise.all(transportStops.map((stop) => stop()));
704
934
  await Promise.all(facades.map((facade) => facade.close()));
705
935
  for (const sentinel of sentinels) {
706
936
  try {
@@ -720,6 +950,7 @@ export async function startClaudeNativeBridge({
720
950
  handleProgress(event) {
721
951
  const worker = byPaneName.get(event?.paneName);
722
952
  if (!worker) return;
953
+ if (worker.workerType === "interactive") return;
723
954
  if (event.type === "worker_added" || event.type === "dispatched") {
724
955
  worker.facade.update({
725
956
  state: "running",
@@ -753,6 +984,7 @@ export async function startClaudeNativeBridge({
753
984
  completeWorker(result) {
754
985
  const worker = byPaneName.get(result?.paneName);
755
986
  if (!worker) return;
987
+ if (worker.workerType === "interactive") return;
756
988
  worker.facade.writeOutput(
757
989
  `\r\n${String(result.output || "").slice(0, 4096)}\r\n`,
758
990
  );
@@ -766,6 +998,7 @@ export async function startClaudeNativeBridge({
766
998
  });
767
999
  },
768
1000
  async close() {
1001
+ await Promise.all(transportStops.map((stop) => stop()));
769
1002
  await Promise.all(facades.map((facade) => facade.close()));
770
1003
  await removeRosterWorkers(paths.rosterPath, Object.keys(rosterWorkers));
771
1004
  for (const sentinel of sentinels) {
@@ -1,6 +1,14 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ export function normalizeClaudeBridgeSessionId(bridgeSessionId, short) {
5
+ const value = String(bridgeSessionId || "").trim();
6
+ if (value.startsWith("session_")) return value;
7
+ if (value.startsWith("cse_")) return `session_${value.slice(4)}`;
8
+ if (value) return value;
9
+ return `session_${short}`;
10
+ }
11
+
4
12
  export function buildClaudeSessionProjection({
5
13
  pid,
6
14
  procStart,
@@ -13,6 +21,7 @@ export function buildClaudeSessionProjection({
13
21
  updatedAt = Date.now(),
14
22
  status = "busy",
15
23
  version = "triflux-native-bridge",
24
+ bridgeSessionId,
16
25
  } = {}) {
17
26
  if (!Number.isInteger(pid) || pid <= 0) throw new Error("pid is required");
18
27
  if (!procStart) throw new Error("procStart is required");
@@ -36,7 +45,7 @@ export function buildClaudeSessionProjection({
36
45
  jobId: short,
37
46
  status,
38
47
  updatedAt,
39
- bridgeSessionId: `session_${short}`,
48
+ bridgeSessionId: normalizeClaudeBridgeSessionId(bridgeSessionId, short),
40
49
  };
41
50
  }
42
51
 
@@ -29,10 +29,10 @@ function printStartUsage() {
29
29
  console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
30
30
  console.log(` 사용법: ${WHITE}tfx multi "작업 설명"${RESET}`);
31
31
  console.log(
32
- ` ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}`,
32
+ ` ${WHITE}tfx multi --agents codex,antigravity --lead claude "작업"${RESET}`,
33
33
  );
34
34
  console.log(
35
- ` ${WHITE}tfx multi --teammate-mode headless "작업"${RESET} ${DIM}(psmux 헤드리스, 기본)${RESET}`,
35
+ ` ${WHITE}tfx multi --teammate-mode headless "작업"${RESET} ${DIM}(headless workers)${RESET}`,
36
36
  );
37
37
  console.log(
38
38
  ` ${WHITE}tfx multi --dashboard-layout lite "작업"${RESET} ${DIM}(dashboard-lite 기본 뷰)${RESET}`,
@@ -66,10 +66,7 @@ function renderTmuxInstallHelp() {
66
66
  console.log(`\n ${RED}${BOLD}tmux 미발견${RESET}\n`);
67
67
  console.log(" 현재 선택한 모드는 tmux 기반 팀세션이 필요합니다.\n");
68
68
  console.log(
69
- ` 설치:\n WSL2: ${WHITE}wsl sudo apt install tmux${RESET}\n macOS: ${WHITE}brew install tmux${RESET}\n Linux: ${WHITE}apt install tmux${RESET}\n`,
70
- );
71
- console.log(
72
- ` Windows에서는 WSL2를 권장합니다:\n 1. ${WHITE}wsl --install${RESET}\n 2. ${WHITE}wsl sudo apt install tmux${RESET}\n 3. ${WHITE}tfx multi "작업"${RESET}\n`,
69
+ ` 설치:\n Windows: ${WHITE}winget install psmux${RESET}\n macOS: ${WHITE}brew install tmux${RESET}\n Linux: ${WHITE}apt install tmux${RESET}\n`,
73
70
  );
74
71
  }
75
72
 
@@ -9,6 +9,8 @@ import {
9
9
  // --assign 파싱 시 마지막 콜론 뒤를 role로 인식할 알려진 역할/CLI 이름
10
10
  const KNOWN_ROLES = new Set([
11
11
  "codex",
12
+ "antigravity",
13
+ "agy",
12
14
  "gemini",
13
15
  "claude",
14
16
  "executor",
@@ -26,7 +28,12 @@ const KNOWN_ROLES = new Set([
26
28
  "scientist",
27
29
  ]);
28
30
 
29
- const NATIVE_BRIDGE_MODES = new Set(["roster", "agents", "claude-wrapper"]);
31
+ const NATIVE_BRIDGE_MODES = new Set([
32
+ "roster",
33
+ "agents",
34
+ "interactive-attach",
35
+ "claude-wrapper",
36
+ ]);
30
37
 
31
38
  /**
32
39
  * --assign "cli:prompt:role" 형식을 콜론-안전하게 파싱한다.
@@ -59,7 +66,7 @@ function parseAssignValue(raw) {
59
66
  }
60
67
 
61
68
  export function parseTeamArgs(args = []) {
62
- let agents = ["codex", "gemini"];
69
+ let agents = ["codex", "antigravity"];
63
70
  let lead = "claude";
64
71
  let layout = "2x2";
65
72
  let teammateMode = "auto";
@@ -153,7 +160,7 @@ export function parseTeamArgs(args = []) {
153
160
  const mode = args[++index];
154
161
  if (!NATIVE_BRIDGE_MODES.has(mode)) {
155
162
  throw new Error(
156
- `unknown native bridge mode: ${mode || ""}; expected roster, agents, or claude-wrapper`,
163
+ `unknown native bridge mode: ${mode || ""}; expected roster, agents, interactive-attach, or claude-wrapper`,
157
164
  );
158
165
  }
159
166
  nativeBridge = true;
@@ -1,9 +1,50 @@
1
- import { resolveCliType, runHeadlessInteractive } from "../../../headless.mjs";
1
+ import {
2
+ normalizeHeadlessRole,
3
+ resolveCliType,
4
+ resolveHeadlessDisplayName,
5
+ runHeadlessInteractive,
6
+ } from "../../../headless.mjs";
2
7
  import { AMBER, DIM, GREEN, RESET } from "../../../shared.mjs";
3
8
  import { ok } from "../../render.mjs";
4
9
  import { clearTeamState } from "../../services/state-store.mjs";
5
10
  import { buildTasks } from "../../services/task-model.mjs";
6
11
 
12
+ export function buildHeadlessAssignments({
13
+ agents = [],
14
+ subtasks = [],
15
+ assigns = [],
16
+ mcpProfile,
17
+ model,
18
+ cwd,
19
+ } = {}) {
20
+ // --assign이 있으면 그것을 사용, 없으면 agents+subtasks 조합
21
+ return assigns && assigns.length > 0
22
+ ? assigns.map((a, i) => {
23
+ const paneName = `worker-${i + 1}`;
24
+ return {
25
+ cli: resolveCliType(a.cli),
26
+ prompt: a.prompt,
27
+ displayName: resolveHeadlessDisplayName(a, paneName),
28
+ role: normalizeHeadlessRole(a.role),
29
+ mcp: mcpProfile,
30
+ model,
31
+ cwd,
32
+ };
33
+ })
34
+ : subtasks.map((subtask, i) => {
35
+ const paneName = `worker-${i + 1}`;
36
+ return {
37
+ cli: resolveCliType(agents[i] || agents[0]),
38
+ prompt: subtask,
39
+ displayName: paneName,
40
+ role: "",
41
+ mcp: mcpProfile,
42
+ model,
43
+ cwd,
44
+ };
45
+ });
46
+ }
47
+
7
48
  export async function startHeadlessTeam({
8
49
  sessionId,
9
50
  task,
@@ -26,25 +67,14 @@ export async function startHeadlessTeam({
26
67
  nativeBridge,
27
68
  nativeBridgeMode,
28
69
  }) {
29
- // --assign이 있으면 그것을 사용, 없으면 agents+subtasks 조합
30
- const assignments =
31
- assigns && assigns.length > 0
32
- ? assigns.map((a, i) => ({
33
- cli: resolveCliType(a.cli),
34
- prompt: a.prompt,
35
- role: a.role || `worker-${i + 1}`,
36
- mcp: mcpProfile,
37
- model,
38
- cwd,
39
- }))
40
- : subtasks.map((subtask, i) => ({
41
- cli: resolveCliType(agents[i] || agents[0]),
42
- prompt: subtask,
43
- role: `worker-${i + 1}`,
44
- mcp: mcpProfile,
45
- model,
46
- cwd,
47
- }));
70
+ const assignments = buildHeadlessAssignments({
71
+ agents,
72
+ subtasks,
73
+ assigns,
74
+ mcpProfile,
75
+ model,
76
+ cwd,
77
+ });
48
78
 
49
79
  const _startedAt = Date.now();
50
80
  ok(`headless ${assignments.length}워커 시작`);
@@ -6,7 +6,7 @@ export function renderTeamHelp() {
6
6
 
7
7
  ${BOLD}시작${RESET}
8
8
  ${WHITE}tfx multi "작업 설명"${RESET}
9
- ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}
9
+ ${WHITE}tfx multi --agents codex,antigravity --lead claude "작업"${RESET}
10
10
  ${WHITE}tfx multi --teammate-mode tmux "작업"${RESET}
11
11
  ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
12
12
  ${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
@@ -9,12 +9,15 @@ export function normalizeTeammateMode(mode = "auto") {
9
9
  const raw = String(mode).toLowerCase();
10
10
  if (raw === "inline" || raw === "native") return "in-process";
11
11
  if (raw === "headless" || raw === "hl") return "headless";
12
- if (raw === "psmux") return "headless";
13
- if (raw === "in-process" || raw === "tmux" || raw === "wt") return raw;
12
+ if (raw === "in-process" || raw === "tmux" || raw === "psmux" || raw === "wt")
13
+ return raw;
14
14
  if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
15
15
  if (raw === "auto") {
16
16
  if (process.env.TMUX) return "tmux";
17
- return detectMultiplexer() === "psmux" ? "headless" : "in-process";
17
+ const mux = detectMultiplexer();
18
+ if (mux === "psmux") return "psmux";
19
+ if (mux === "tmux") return "tmux";
20
+ return "in-process";
18
21
  }
19
22
  return "in-process";
20
23
  }