@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
|
@@ -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 =
|
|
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)
|
|
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, {
|
|
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:
|
|
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,
|
|
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}(
|
|
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
|
|
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([
|
|
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", "
|
|
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 {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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}워커 시작`);
|
package/hub/team/cli/help.mjs
CHANGED
|
@@ -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,
|
|
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"
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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 === "
|
|
119
|
-
const args = [];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 === "
|
|
211
|
+
if (cli === "antigravity") {
|
|
197
212
|
return {
|
|
198
213
|
mode: MODES.HEADLESS,
|
|
199
|
-
reason: "
|
|
214
|
+
reason: "antigravity CLI uses headless stdin print mode",
|
|
200
215
|
};
|
|
201
216
|
}
|
|
202
217
|
|