@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.
- 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 +244 -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 +63 -14
- 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,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)
|
|
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, {
|
|
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:
|
|
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
|
}
|