@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
@@ -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,6 +645,10 @@ 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);
@@ -562,8 +706,25 @@ export async function startNativeWorkerFacade({
562
706
  ? frame.ctrl
563
707
  : { kind: 0, bytes: frame.payload.length },
564
708
  });
565
- if (frame.kind === 0) appendOutput(frame.payload);
709
+ if (frame.kind === 0) handlePtyInput(frame.payload);
566
710
  else if (frame.ctrl.t === "resize") {
711
+ if (
712
+ workerType === "interactive" &&
713
+ typeof onResize === "function"
714
+ ) {
715
+ const cols = toPositiveInteger(frame.ctrl.cols);
716
+ const rows = toPositiveInteger(frame.ctrl.rows);
717
+ if (cols && rows) {
718
+ void Promise.resolve()
719
+ .then(() => onResize({ cols, rows }))
720
+ .catch((error) => {
721
+ events.push({
722
+ type: "pty-resize-error",
723
+ message: error.message,
724
+ });
725
+ });
726
+ }
727
+ }
567
728
  socket.write(buildPtyControlFrame({ t: "live" }));
568
729
  } else if (frame.ctrl.t === "kill") {
569
730
  socket.write(
@@ -633,6 +794,10 @@ export async function startClaudeNativeBridge({
633
794
  procStart,
634
795
  tmpRoot = os.tmpdir(),
635
796
  onKill,
797
+ workerType = "headless",
798
+ launchCmd = "codex",
799
+ env = {},
800
+ createTransport = createInteractiveTuiTransport,
636
801
  } = {}) {
637
802
  if (!sessionName) throw new Error("sessionName is required");
638
803
  const paths = deriveClaudeDaemonPaths({ configDir, tmpRoot: "/tmp" });
@@ -647,6 +812,7 @@ export async function startClaudeNativeBridge({
647
812
  const startedAt = Date.now();
648
813
  const facades = [];
649
814
  const sentinels = [];
815
+ const transportStops = [];
650
816
  const rosterWorkers = {};
651
817
  const byPaneName = new Map();
652
818
 
@@ -675,15 +841,66 @@ export async function startClaudeNativeBridge({
675
841
  if (!workerPid) throw new Error("native bridge sentinel pid unavailable");
676
842
  const rvSock = path.join(socketRoot, `${short}.rv.sock`);
677
843
  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
844
  const workerCwd = assignment.cwd || assignment.workdir || cwd;
845
+ const assignmentWorkerType = normalizeWorkerType(
846
+ assignment.workerType || workerType,
847
+ );
848
+ let facade = null;
849
+ if (assignmentWorkerType === "interactive") {
850
+ const transport = createTransport({
851
+ sessionName: buildInteractiveTransportSessionName(
852
+ sessionName,
853
+ paneName,
854
+ short,
855
+ ),
856
+ cwd: workerCwd,
857
+ launchCmd: assignment.launchCmd || launchCmd,
858
+ env: assignment.env || env,
859
+ onData(chunk) {
860
+ if (facade) facade.writeOutput(chunk);
861
+ },
862
+ });
863
+ const stopTransport = createStopOnce(transport);
864
+ transportStops.push(stopTransport);
865
+ facade = await startNativeWorkerFacade({
866
+ short,
867
+ rvSock,
868
+ ptySock,
869
+ pid: workerPid,
870
+ initialDetail: `${cli}:${role}:interactive`,
871
+ workerType: "interactive",
872
+ onInput(payload) {
873
+ void Promise.resolve()
874
+ .then(() => transport.writeInput(payload))
875
+ .catch(() => {});
876
+ },
877
+ onResize(size) {
878
+ void Promise.resolve()
879
+ .then(() => transport.resize(size))
880
+ .catch(() => {});
881
+ },
882
+ onKill(event) {
883
+ void stopTransport();
884
+ if (typeof onKill === "function") onKill(event);
885
+ },
886
+ });
887
+ try {
888
+ await transport.start();
889
+ } catch (error) {
890
+ await stopTransport();
891
+ await facade.close({ exitCode: 1 }).catch(() => {});
892
+ throw error;
893
+ }
894
+ } else {
895
+ facade = await startNativeWorkerFacade({
896
+ short,
897
+ rvSock,
898
+ ptySock,
899
+ pid: workerPid,
900
+ initialDetail: `${cli}:${role}:pending`,
901
+ onKill,
902
+ });
903
+ }
687
904
  rosterWorkers[short] = buildNativeWorkerRosterEntry({
688
905
  short,
689
906
  pid: workerPid,
@@ -696,11 +913,18 @@ export async function startClaudeNativeBridge({
696
913
  prompt: assignment.prompt || "",
697
914
  });
698
915
  facades.push(facade);
699
- byPaneName.set(paneName, { facade, cli, role, short });
916
+ byPaneName.set(paneName, {
917
+ facade,
918
+ cli,
919
+ role,
920
+ short,
921
+ workerType: assignmentWorkerType,
922
+ });
700
923
  }
701
924
 
702
925
  await mergeRosterWorkers(paths.rosterPath, rosterWorkers);
703
926
  } catch (error) {
927
+ await Promise.all(transportStops.map((stop) => stop()));
704
928
  await Promise.all(facades.map((facade) => facade.close()));
705
929
  for (const sentinel of sentinels) {
706
930
  try {
@@ -720,6 +944,7 @@ export async function startClaudeNativeBridge({
720
944
  handleProgress(event) {
721
945
  const worker = byPaneName.get(event?.paneName);
722
946
  if (!worker) return;
947
+ if (worker.workerType === "interactive") return;
723
948
  if (event.type === "worker_added" || event.type === "dispatched") {
724
949
  worker.facade.update({
725
950
  state: "running",
@@ -753,6 +978,7 @@ export async function startClaudeNativeBridge({
753
978
  completeWorker(result) {
754
979
  const worker = byPaneName.get(result?.paneName);
755
980
  if (!worker) return;
981
+ if (worker.workerType === "interactive") return;
756
982
  worker.facade.writeOutput(
757
983
  `\r\n${String(result.output || "").slice(0, 4096)}\r\n`,
758
984
  );
@@ -766,6 +992,7 @@ export async function startClaudeNativeBridge({
766
992
  });
767
993
  },
768
994
  async close() {
995
+ await Promise.all(transportStops.map((stop) => stop()));
769
996
  await Promise.all(facades.map((facade) => facade.close()));
770
997
  await removeRosterWorkers(paths.rosterPath, Object.keys(rosterWorkers));
771
998
  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
  }
@@ -8,7 +8,7 @@ import { whichCommand } from "@triflux/core/hub/platform.mjs";
8
8
  const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
9
9
 
10
10
  // #128: parse npm-cmd-shim wrapper to extract the underlying .js entry point.
11
- // codex.cmd / gemini.cmd end with: "%_prog%" "%dp0%\node_modules\...\<cli>.js" %*
11
+ // npm shim wrappers end with: "%_prog%" "%dp0%\node_modules\...\<cli>.js" %*
12
12
  // %* is cmd batch arg pass-through which mangles multi-line / fenced prompts.
13
13
  // Bypass cmd entirely by spawning node + the .js directly.
14
14
  export function unwrapCmdToJsScript(cmdPath, opts = {}) {
@@ -55,6 +55,14 @@ export function resolveStdinPromptMode(explicit, envSource) {
55
55
  return true;
56
56
  }
57
57
 
58
+ function normalizeCli(cli) {
59
+ const value = String(cli || "codex").toLowerCase();
60
+ if (value === "gemini" || value === "antigravity" || value === "agy") {
61
+ return "antigravity";
62
+ }
63
+ return value;
64
+ }
65
+
58
66
  export function resolveCliExecutable(cli, opts = {}) {
59
67
  const name = String(cli || "codex");
60
68
  const resolveCommand = opts.resolveCommand || whichCommand;
@@ -86,9 +94,10 @@ export function resolveCliExecutable(cli, opts = {}) {
86
94
  }
87
95
 
88
96
  export function buildSpawnSpecForMode(mode, opts = {}) {
89
- const cli = opts.cli || "codex";
97
+ const cli = normalizeCli(opts.cli || "codex");
90
98
  const prompt = asPrompt(opts.prompt);
91
- const resolvedCommand = resolveCliExecutable(cli, opts);
99
+ const commandName = cli === "antigravity" ? "agy" : cli;
100
+ const resolvedCommand = resolveCliExecutable(commandName, opts);
92
101
  const platform = opts.platform || process.platform;
93
102
 
94
103
  // Node v20.12+ (CVE-2024-27980) rejects spawn of .cmd/.bat files with shell:false
@@ -115,11 +124,15 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
115
124
  return { command: resolvedCommand, args };
116
125
  };
117
126
 
118
- if (cli === "gemini") {
119
- const args = [];
120
- pushFlag(args, "--model", opts.model);
121
- args.push("--yolo", "--prompt", prompt, "--output-format", "text");
122
- return { ...wrap(args), useExec: true, shell: false };
127
+ if (cli === "antigravity") {
128
+ const args = ["--print", "--dangerously-skip-permissions"];
129
+ return {
130
+ ...wrap(args),
131
+ useExec: true,
132
+ shell: false,
133
+ stdinPrompt: prompt.length > 0,
134
+ prompt,
135
+ };
123
136
  }
124
137
 
125
138
  if (mode === MODES.INTERACTIVE || mode === MODES.AUTO) {
@@ -169,7 +182,7 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
169
182
 
170
183
  /**
171
184
  * @param {{
172
- * cli: "codex"|"gemini"|"claude",
185
+ * cli: "codex"|"gemini"|"antigravity"|"agy"|"claude",
173
186
  * taskType?: "implement"|"review"|"research"|"test"|"analyze",
174
187
  * needsInput?: boolean,
175
188
  * estimatedDuration?: number,
@@ -179,13 +192,15 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
179
192
  */
180
193
  export function selectExecutionMode(opts = {}) {
181
194
  const {
182
- cli = "codex",
195
+ cli: rawCli = "codex",
183
196
  taskType = "research",
184
197
  needsInput = false,
185
198
  estimatedDuration = 0,
186
199
  hasHub = false,
187
200
  } = opts;
188
201
 
202
+ const cli = normalizeCli(rawCli);
203
+
189
204
  if (!hasHub) {
190
205
  return {
191
206
  mode: MODES.HEADLESS,
@@ -193,10 +208,10 @@ export function selectExecutionMode(opts = {}) {
193
208
  };
194
209
  }
195
210
 
196
- if (cli === "gemini") {
211
+ if (cli === "antigravity") {
197
212
  return {
198
213
  mode: MODES.HEADLESS,
199
- reason: "gemini CLI only supports headless prompt mode",
214
+ reason: "antigravity CLI uses headless stdin print mode",
200
215
  };
201
216
  }
202
217