@triflux/remote 10.51.0 → 10.52.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/cto/brief.mjs +0 -8
- package/cto/collect.mjs +1 -127
- package/cto/current.schema.json +2 -19
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -139
- package/hub/index.mjs +0 -9
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/debug.mjs +1 -2
- package/hub/team/cli/commands/start/index.mjs +2 -1
- package/hub/team/cli/commands/start/start-mux.mjs +2 -4
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/headless.mjs +96 -543
- package/hub/team/psmux.mjs +3 -33
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/tui-viewer.mjs +9 -56
- package/hub/team/tui.mjs +0 -37
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/workers/factory.mjs +6 -51
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +0 -67
- package/scripts/lib/cli-claude.mjs +0 -12
- package/scripts/lib/env-probe.mjs +0 -70
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/cli/services/hub-client.mjs +0 -178
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-intent.mjs +0 -18
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/tools.mjs +0 -900
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -26
- package/scripts/lib/context.mjs +0 -51
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
package/hub/team/headless.mjs
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
writeFileSync,
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
|
-
import net from "node:net";
|
|
16
15
|
import { tmpdir } from "node:os";
|
|
17
16
|
import { dirname, join, resolve } from "node:path";
|
|
18
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -25,26 +24,15 @@ import {
|
|
|
25
24
|
resolveStallInterventionMs,
|
|
26
25
|
} from "@triflux/core/hub/lib/worker-lifecycle.mjs";
|
|
27
26
|
import { IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
|
|
27
|
+
import { createAgentsRowOwnership, openAgentsRow } from "./agents-row.mjs";
|
|
28
28
|
import { getBackend } from "./backend.mjs";
|
|
29
|
-
import {
|
|
30
|
-
buildDaemonControlAuth,
|
|
31
|
-
buildDaemonExecDispatchPayload,
|
|
32
|
-
deriveClaudeDaemonPaths as deriveClaudeControlPaths,
|
|
33
|
-
dispatchClaudeDaemonJob,
|
|
34
|
-
killDaemonJob,
|
|
35
|
-
sendKillBySessionId,
|
|
36
|
-
} from "./claude-daemon-control.mjs";
|
|
37
|
-
import { removeClaudeSessionProjection } from "./claude-session-projection.mjs";
|
|
38
29
|
import { resolveDashboardLayout } from "./dashboard-layout.mjs";
|
|
39
30
|
import {
|
|
40
31
|
formatHandoffForLead,
|
|
41
32
|
HANDOFF_INSTRUCTION_SHORT,
|
|
42
33
|
processHandoff,
|
|
43
34
|
} from "./handoff.mjs";
|
|
44
|
-
import {
|
|
45
|
-
createFileActivitySource,
|
|
46
|
-
createInterventionLadder,
|
|
47
|
-
} from "./intervention.mjs";
|
|
35
|
+
import { createInterventionLadder } from "./intervention.mjs";
|
|
48
36
|
import {
|
|
49
37
|
capturePsmuxPane,
|
|
50
38
|
createPsmuxSession,
|
|
@@ -52,7 +40,6 @@ import {
|
|
|
52
40
|
killPsmuxSession,
|
|
53
41
|
psmuxExec,
|
|
54
42
|
psmuxSessionExists,
|
|
55
|
-
sendKeysToPane,
|
|
56
43
|
startCapture,
|
|
57
44
|
waitForCompletion,
|
|
58
45
|
} from "./psmux.mjs";
|
|
@@ -60,7 +47,6 @@ import { createLogDashboard } from "./tui.mjs";
|
|
|
60
47
|
import { createWtManager } from "./wt-manager.mjs";
|
|
61
48
|
|
|
62
49
|
const RESULT_DIR = join(tmpdir(), "tfx-headless");
|
|
63
|
-
const DAEMON_COMPLETION_PREFIX = "__TFX_HEADLESS_DONE__:";
|
|
64
50
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
65
51
|
|
|
66
52
|
/** CLI별 브랜드 — 이모지 + 공식 색상 (HUD와 통일) */
|
|
@@ -235,7 +221,11 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
235
221
|
let ctx = readFileSync(contextFile, "utf8");
|
|
236
222
|
let truncated = false;
|
|
237
223
|
if (Buffer.byteLength(ctx, "utf8") > 32768) {
|
|
238
|
-
|
|
224
|
+
// 바이트 경계에서 잘린 다바이트 글자는 U+FFFD 로 디코딩되므로 떼어 낸다.
|
|
225
|
+
ctx = Buffer.from(ctx)
|
|
226
|
+
.subarray(0, 32768)
|
|
227
|
+
.toString("utf8")
|
|
228
|
+
.replace(/�+$/u, "");
|
|
239
229
|
truncated = true;
|
|
240
230
|
}
|
|
241
231
|
if (truncated) {
|
|
@@ -309,48 +299,6 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
309
299
|
return `cd '${safeCwd.replace(/'/g, "'\\''")}' && ${backendCommand}`;
|
|
310
300
|
}
|
|
311
301
|
|
|
312
|
-
function escapeRegExp(value) {
|
|
313
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function buildDaemonWrappedCommand(command, token) {
|
|
317
|
-
return `{ ${command}; __ec=$?; echo "${DAEMON_COMPLETION_PREFIX}${token}:$__ec"; }`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export function waitForDaemonCompletionFromMessages(
|
|
321
|
-
messages,
|
|
322
|
-
{ token, resultFile } = {},
|
|
323
|
-
) {
|
|
324
|
-
const completionRegex = new RegExp(
|
|
325
|
-
`${escapeRegExp(DAEMON_COMPLETION_PREFIX)}${escapeRegExp(token)}:(\\d+)`,
|
|
326
|
-
);
|
|
327
|
-
const stream = [];
|
|
328
|
-
let matched = false;
|
|
329
|
-
let exitCode = null;
|
|
330
|
-
|
|
331
|
-
for (const message of messages) {
|
|
332
|
-
if (Array.isArray(message?.streamTail)) {
|
|
333
|
-
stream.push(...message.streamTail.map((line) => `${line}\n`));
|
|
334
|
-
}
|
|
335
|
-
if (typeof message?.line !== "string") continue;
|
|
336
|
-
const match = completionRegex.exec(message.line);
|
|
337
|
-
if (match) {
|
|
338
|
-
const beforeCompletion = message.line.slice(0, match.index);
|
|
339
|
-
if (beforeCompletion) stream.push(beforeCompletion);
|
|
340
|
-
matched = true;
|
|
341
|
-
exitCode = Number.parseInt(match[1], 10);
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
stream.push(message.line);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (resultFile) {
|
|
348
|
-
writeFileSync(`${resultFile}.partial`, stream.join(""), "utf8");
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return { matched, exitCode };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
302
|
/**
|
|
355
303
|
* 이전 run 의 stale .txt / .partial / .err 를 제거한다.
|
|
356
304
|
* Issue #118 Codex review R1 HIGH: resultFile 경로 재사용 시 (워커 restart 또는
|
|
@@ -413,43 +361,28 @@ function createHeadlessIntervention(dispatch, fallback) {
|
|
|
413
361
|
if (typeof fallback === "function")
|
|
414
362
|
return (await fallback(context)) === true;
|
|
415
363
|
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
return `${capturePsmuxPane(context.paneId || dispatch.paneId, 50)}\0${mtime}`;
|
|
427
|
-
};
|
|
364
|
+
const readActivitySignature = () => {
|
|
365
|
+
let mtime = 0;
|
|
366
|
+
try {
|
|
367
|
+
mtime = statSync(dispatch.resultFile).mtimeMs;
|
|
368
|
+
} catch {
|
|
369
|
+
/* result file has not been written yet */
|
|
370
|
+
}
|
|
371
|
+
return `${capturePsmuxPane(context.paneId || dispatch.paneId, 50)}\0${mtime}`;
|
|
372
|
+
};
|
|
428
373
|
const ladder = createInterventionLadder({
|
|
429
|
-
target:
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
controlSock: dispatch.controlSock,
|
|
436
|
-
short: dispatch.daemonShort,
|
|
437
|
-
sessionId: dispatch.sessionId,
|
|
438
|
-
configDir: dispatch.daemonPaths?.configDir,
|
|
439
|
-
},
|
|
440
|
-
}
|
|
441
|
-
: {
|
|
442
|
-
channel: "tmux-pane",
|
|
443
|
-
paneId: context.paneId || dispatch.paneId,
|
|
444
|
-
cli: dispatch.cli,
|
|
445
|
-
sessionId: dispatch.sessionId,
|
|
446
|
-
},
|
|
374
|
+
target: {
|
|
375
|
+
channel: "tmux-pane",
|
|
376
|
+
paneId: context.paneId || dispatch.paneId,
|
|
377
|
+
cli: dispatch.cli,
|
|
378
|
+
sessionId: dispatch.sessionId,
|
|
379
|
+
},
|
|
447
380
|
readActivitySignature,
|
|
448
381
|
deps: {
|
|
449
382
|
// 이 트랙이 재실행 소유자다. pane 채널은 기존 completion wrapper를 다시
|
|
450
383
|
// dispatch하여 token/result 계약을 보존한다.
|
|
451
384
|
resumeHandler: async () => {
|
|
452
|
-
if (
|
|
385
|
+
if (!dispatch.command) return { ok: false };
|
|
453
386
|
const restarted = dispatchCommand(
|
|
454
387
|
context.sessionName,
|
|
455
388
|
context.paneId || dispatch.paneId,
|
|
@@ -752,133 +685,6 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
752
685
|
);
|
|
753
686
|
}
|
|
754
687
|
|
|
755
|
-
async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
756
|
-
const { safeProgress, configDir } = opts;
|
|
757
|
-
const paths = deriveClaudeControlPaths({ configDir });
|
|
758
|
-
const dispatches = [];
|
|
759
|
-
|
|
760
|
-
try {
|
|
761
|
-
for (let i = 0; i < assignments.length; i += 1) {
|
|
762
|
-
const assignment = assignments[i];
|
|
763
|
-
const paneName = `worker-${i + 1}`;
|
|
764
|
-
const displayName = resolveHeadlessDisplayName(assignment, paneName);
|
|
765
|
-
const workerId = getHeadlessWorkerAgentId(sessionName, i);
|
|
766
|
-
const resolvedCli = resolveCliType(assignment.cli);
|
|
767
|
-
const resultFile = join(
|
|
768
|
-
RESULT_DIR,
|
|
769
|
-
`${sessionName}-${paneName}.txt`,
|
|
770
|
-
).replace(/\\/g, "/");
|
|
771
|
-
cleanStaleResultArtifacts(resultFile);
|
|
772
|
-
const command = buildHeadlessCommand(
|
|
773
|
-
assignment.cli,
|
|
774
|
-
assignment.prompt,
|
|
775
|
-
resultFile,
|
|
776
|
-
{
|
|
777
|
-
mcp: assignment.mcp,
|
|
778
|
-
role: assignment.role,
|
|
779
|
-
model: assignment.model,
|
|
780
|
-
profile: assignment.profile,
|
|
781
|
-
cwd: assignment.cwd || assignment.workdir,
|
|
782
|
-
},
|
|
783
|
-
);
|
|
784
|
-
const token = randomUUID().slice(0, 10);
|
|
785
|
-
const short = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
786
|
-
const name = `Triflux ${resolvedCli || "worker"} ${displayName}`;
|
|
787
|
-
const record = {
|
|
788
|
-
paneId: `daemon:${short}`,
|
|
789
|
-
paneName,
|
|
790
|
-
displayName,
|
|
791
|
-
resultFile,
|
|
792
|
-
cli: resolvedCli,
|
|
793
|
-
role: assignment.role,
|
|
794
|
-
command,
|
|
795
|
-
token,
|
|
796
|
-
workerId,
|
|
797
|
-
cwd: assignment.cwd || assignment.workdir,
|
|
798
|
-
daemonShort: short,
|
|
799
|
-
daemonPaths: paths,
|
|
800
|
-
controlSock: paths.controlSock,
|
|
801
|
-
sessionId: "",
|
|
802
|
-
sessionProjectionPath: "",
|
|
803
|
-
daemonCompletionMatched: false,
|
|
804
|
-
};
|
|
805
|
-
dispatches.push(record);
|
|
806
|
-
|
|
807
|
-
const payload = buildDaemonExecDispatchPayload({
|
|
808
|
-
short,
|
|
809
|
-
cwd: assignment.cwd || assignment.workdir || process.cwd(),
|
|
810
|
-
command: buildDaemonWrappedCommand(command, token),
|
|
811
|
-
name,
|
|
812
|
-
});
|
|
813
|
-
record.sessionId = payload.sessionId;
|
|
814
|
-
// buildDaemonWrappedCommand + token completion 시맨틱은 헬퍼 밖(headless 전용)에
|
|
815
|
-
// 남기고, dispatch→pid→bridge→projection 시퀀스만 공유 헬퍼로 위임한다.
|
|
816
|
-
// CRITICAL: waitForDaemonCompletion 이 나중에 record.daemonCompletionMatched 를
|
|
817
|
-
// 변경하고 cleanupDaemonDispatches 가 같은 record 를 읽으므로, 헬퍼가 새 객체를
|
|
818
|
-
// 반환해도 record 를 교체하지 말고 plain 필드를 Object.assign 한다.
|
|
819
|
-
const dispatched = await dispatchClaudeDaemonJob({
|
|
820
|
-
paths,
|
|
821
|
-
controlSock: paths.controlSock,
|
|
822
|
-
payload,
|
|
823
|
-
agent: resolvedCli || "codex",
|
|
824
|
-
name,
|
|
825
|
-
cwd: assignment.cwd || assignment.workdir || process.cwd(),
|
|
826
|
-
dispatchTimeoutMs: 5000,
|
|
827
|
-
}).catch((error) => {
|
|
828
|
-
throw new Error(
|
|
829
|
-
`[headless] Claude daemon dispatch failed for ${paneName}: ${
|
|
830
|
-
error?.message || error
|
|
831
|
-
}`,
|
|
832
|
-
);
|
|
833
|
-
});
|
|
834
|
-
Object.assign(record, {
|
|
835
|
-
sessionId: dispatched.sessionId,
|
|
836
|
-
sessionProjectionPath: dispatched.sessionProjectionPath,
|
|
837
|
-
});
|
|
838
|
-
if (safeProgress) {
|
|
839
|
-
safeProgress({ type: "dispatched", paneName, cli: resolvedCli });
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
} catch (error) {
|
|
843
|
-
await cleanupDaemonDispatches(dispatches);
|
|
844
|
-
throw error;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
return dispatches;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
export async function cleanupDaemonDispatches(dispatches) {
|
|
851
|
-
await Promise.all(
|
|
852
|
-
dispatches.map(async (dispatch) => {
|
|
853
|
-
if (dispatch.sessionProjectionPath) {
|
|
854
|
-
await removeClaudeSessionProjection(
|
|
855
|
-
dispatch.sessionProjectionPath,
|
|
856
|
-
).catch(() => {});
|
|
857
|
-
}
|
|
858
|
-
if (
|
|
859
|
-
dispatch.daemonCompletionMatched === true &&
|
|
860
|
-
dispatch.daemonPaths &&
|
|
861
|
-
dispatch.sessionId
|
|
862
|
-
) {
|
|
863
|
-
await sendKillBySessionId({
|
|
864
|
-
daemonPaths: dispatch.daemonPaths,
|
|
865
|
-
sessionId: dispatch.sessionId,
|
|
866
|
-
}).catch(() => {});
|
|
867
|
-
}
|
|
868
|
-
if (dispatch.controlSock && dispatch.daemonShort) {
|
|
869
|
-
const controlAuth = await buildDaemonControlAuth(
|
|
870
|
-
dispatch.daemonPaths?.configDir,
|
|
871
|
-
).catch(() => ({}));
|
|
872
|
-
await killDaemonJob(
|
|
873
|
-
dispatch.controlSock,
|
|
874
|
-
dispatch.daemonShort,
|
|
875
|
-
controlAuth,
|
|
876
|
-
).catch(() => {});
|
|
877
|
-
}
|
|
878
|
-
}),
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
688
|
/**
|
|
883
689
|
* 모든 dispatch를 병렬 대기하며 완료 결과를 수집한다.
|
|
884
690
|
* @param {string} sessionName
|
|
@@ -998,169 +804,6 @@ async function awaitAll(
|
|
|
998
804
|
);
|
|
999
805
|
}
|
|
1000
806
|
|
|
1001
|
-
export async function waitForDaemonCompletion(
|
|
1002
|
-
dispatch,
|
|
1003
|
-
timeoutSec,
|
|
1004
|
-
safeProgress,
|
|
1005
|
-
progressIntervalSec,
|
|
1006
|
-
lifecycle = null,
|
|
1007
|
-
) {
|
|
1008
|
-
const messages = [];
|
|
1009
|
-
const socket = net.connect(dispatch.controlSock);
|
|
1010
|
-
let buffer = "";
|
|
1011
|
-
let settled = false;
|
|
1012
|
-
let lastProgressAt = 0;
|
|
1013
|
-
const activity = lifecycle?.enabled
|
|
1014
|
-
? createActivityLifecycle({
|
|
1015
|
-
interventionMs: lifecycle.interventionMs,
|
|
1016
|
-
hardCeilingMs: lifecycle.hardCeilingMs,
|
|
1017
|
-
onIntervene: createHeadlessIntervention(
|
|
1018
|
-
dispatch,
|
|
1019
|
-
lifecycle.onIntervene,
|
|
1020
|
-
),
|
|
1021
|
-
})
|
|
1022
|
-
: null;
|
|
1023
|
-
|
|
1024
|
-
return await new Promise((resolve) => {
|
|
1025
|
-
const finish = (completion, timeoutReason = "") => {
|
|
1026
|
-
if (settled) return;
|
|
1027
|
-
settled = true;
|
|
1028
|
-
clearTimeout(timer);
|
|
1029
|
-
clearInterval(timer);
|
|
1030
|
-
socket.destroy();
|
|
1031
|
-
const finalCompletion =
|
|
1032
|
-
completion ||
|
|
1033
|
-
waitForDaemonCompletionFromMessages(messages, {
|
|
1034
|
-
token: dispatch.token,
|
|
1035
|
-
resultFile: dispatch.resultFile,
|
|
1036
|
-
});
|
|
1037
|
-
if (!finalCompletion.matched && timeoutReason) {
|
|
1038
|
-
finalCompletion.timedOut = true;
|
|
1039
|
-
finalCompletion.timeoutReason = timeoutReason;
|
|
1040
|
-
}
|
|
1041
|
-
if (finalCompletion.matched) {
|
|
1042
|
-
dispatch.daemonCompletionMatched = true;
|
|
1043
|
-
cleanupDaemonDispatches([dispatch]).catch(() => {});
|
|
1044
|
-
} else {
|
|
1045
|
-
buildDaemonControlAuth(dispatch.daemonPaths?.configDir)
|
|
1046
|
-
.catch(() => ({}))
|
|
1047
|
-
.then((controlAuth) =>
|
|
1048
|
-
killDaemonJob(
|
|
1049
|
-
dispatch.controlSock,
|
|
1050
|
-
dispatch.daemonShort,
|
|
1051
|
-
controlAuth,
|
|
1052
|
-
),
|
|
1053
|
-
)
|
|
1054
|
-
.catch(() => {});
|
|
1055
|
-
}
|
|
1056
|
-
resolve(finalCompletion);
|
|
1057
|
-
};
|
|
1058
|
-
|
|
1059
|
-
const timer = activity
|
|
1060
|
-
? setInterval(() => {
|
|
1061
|
-
void activity
|
|
1062
|
-
.check({
|
|
1063
|
-
channel: "daemon",
|
|
1064
|
-
controlSock: dispatch.controlSock,
|
|
1065
|
-
daemonShort: dispatch.daemonShort,
|
|
1066
|
-
resultFile: dispatch.resultFile,
|
|
1067
|
-
})
|
|
1068
|
-
.then((reason) => {
|
|
1069
|
-
if (reason) finish(null, reason);
|
|
1070
|
-
});
|
|
1071
|
-
}, lifecycle.checkIntervalMs ?? 5_000)
|
|
1072
|
-
: setTimeout(
|
|
1073
|
-
() => finish(null, "wall_clock"),
|
|
1074
|
-
Math.max(0, timeoutSec * 1000),
|
|
1075
|
-
);
|
|
1076
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
1077
|
-
|
|
1078
|
-
socket.on("error", () => finish(null, "socket_error"));
|
|
1079
|
-
socket.on("close", () => {
|
|
1080
|
-
if (!settled) finish(null, "socket_closed");
|
|
1081
|
-
});
|
|
1082
|
-
socket.on("connect", () => {
|
|
1083
|
-
socket.write(
|
|
1084
|
-
`${JSON.stringify({
|
|
1085
|
-
proto: 1,
|
|
1086
|
-
op: "subscribe",
|
|
1087
|
-
short: dispatch.daemonShort,
|
|
1088
|
-
tail: 20,
|
|
1089
|
-
})}\n`,
|
|
1090
|
-
);
|
|
1091
|
-
});
|
|
1092
|
-
socket.on("data", (chunk) => {
|
|
1093
|
-
buffer += chunk.toString("utf8");
|
|
1094
|
-
while (buffer.includes("\n")) {
|
|
1095
|
-
const index = buffer.indexOf("\n");
|
|
1096
|
-
const line = buffer.slice(0, index);
|
|
1097
|
-
buffer = buffer.slice(index + 1);
|
|
1098
|
-
if (!line.trim()) continue;
|
|
1099
|
-
const message = JSON.parse(line);
|
|
1100
|
-
messages.push(message);
|
|
1101
|
-
activity?.observe();
|
|
1102
|
-
if (
|
|
1103
|
-
safeProgress &&
|
|
1104
|
-
progressIntervalSec > 0 &&
|
|
1105
|
-
typeof message.line === "string"
|
|
1106
|
-
) {
|
|
1107
|
-
const now = Date.now();
|
|
1108
|
-
if (now - lastProgressAt >= progressIntervalSec * 1000) {
|
|
1109
|
-
lastProgressAt = now;
|
|
1110
|
-
safeProgress({
|
|
1111
|
-
type: "progress",
|
|
1112
|
-
paneName: dispatch.paneName,
|
|
1113
|
-
displayName: dispatch.displayName,
|
|
1114
|
-
cli: dispatch.cli,
|
|
1115
|
-
snapshot: message.line.split("\n").slice(-15).join("\n"),
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
const completion = waitForDaemonCompletionFromMessages(messages, {
|
|
1120
|
-
token: dispatch.token,
|
|
1121
|
-
resultFile: dispatch.resultFile,
|
|
1122
|
-
});
|
|
1123
|
-
if (completion.matched) finish(completion);
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
async function awaitAllDaemon(
|
|
1130
|
-
dispatches,
|
|
1131
|
-
timeoutSec,
|
|
1132
|
-
safeProgress,
|
|
1133
|
-
progressIntervalSec,
|
|
1134
|
-
lifecycle,
|
|
1135
|
-
) {
|
|
1136
|
-
return Promise.all(
|
|
1137
|
-
dispatches.map(async (d) => {
|
|
1138
|
-
const completion = await waitForDaemonCompletion(
|
|
1139
|
-
d,
|
|
1140
|
-
timeoutSec,
|
|
1141
|
-
safeProgress,
|
|
1142
|
-
progressIntervalSec,
|
|
1143
|
-
lifecycle,
|
|
1144
|
-
);
|
|
1145
|
-
const output = readResult(d.resultFile, d.paneId);
|
|
1146
|
-
|
|
1147
|
-
if (safeProgress) {
|
|
1148
|
-
safeProgress({
|
|
1149
|
-
type: "completed",
|
|
1150
|
-
paneName: d.paneName,
|
|
1151
|
-
displayName: d.displayName,
|
|
1152
|
-
cli: d.cli,
|
|
1153
|
-
matched: completion.matched,
|
|
1154
|
-
exitCode: completion.exitCode,
|
|
1155
|
-
sessionDead: false,
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
return { d, completion, output };
|
|
1160
|
-
}),
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
807
|
/**
|
|
1165
808
|
* git diff + handoff 파이프라인을 적용하여 최종 결과 배열을 반환한다.
|
|
1166
809
|
* @param {Array<{d, completion, output}>} results
|
|
@@ -1243,20 +886,18 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1243
886
|
const sessionOwnership =
|
|
1244
887
|
opts._sessionOwnership ||
|
|
1245
888
|
createHeadlessSessionOwnership(deps.killPsmuxSession || killPsmuxSession);
|
|
889
|
+
const agentsRows = opts._agentsRows || createAgentsRowOwnership();
|
|
1246
890
|
let {
|
|
1247
891
|
timeoutSec = 900,
|
|
1248
892
|
layout = "2x2",
|
|
1249
893
|
onProgress,
|
|
1250
894
|
progressIntervalSec = 0,
|
|
1251
895
|
progressive = true,
|
|
1252
|
-
autoAttach = false,
|
|
1253
896
|
dashboard = false,
|
|
1254
897
|
dashboardLayout = "single",
|
|
1255
898
|
nativeBridge = false,
|
|
1256
899
|
nativeBridgeMode = "agents",
|
|
1257
900
|
onIntervene,
|
|
1258
|
-
leadPane = null,
|
|
1259
|
-
leadTmux = null,
|
|
1260
901
|
} = opts;
|
|
1261
902
|
if (!Number.isFinite(Number(timeoutSec)) || Number(timeoutSec) <= 0)
|
|
1262
903
|
timeoutSec = 900;
|
|
@@ -1274,10 +915,9 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1274
915
|
);
|
|
1275
916
|
|
|
1276
917
|
if (normalizedAssignments.length === 0) {
|
|
1277
|
-
return { sessionName, results: [], sessionOwnership };
|
|
918
|
+
return { sessionName, results: [], sessionOwnership, agentsRows };
|
|
1278
919
|
}
|
|
1279
920
|
|
|
1280
|
-
let daemonDispatches = [];
|
|
1281
921
|
let runCompleted = false;
|
|
1282
922
|
let completedResults = [];
|
|
1283
923
|
let runFailed = false;
|
|
@@ -1376,79 +1016,36 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1376
1016
|
try {
|
|
1377
1017
|
const progressiveDispatch = deps.dispatchProgressive || dispatchProgressive;
|
|
1378
1018
|
const batchDispatch = deps.dispatchBatch || dispatchBatch;
|
|
1379
|
-
const dispatches =
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
})
|
|
1384
|
-
: progressive
|
|
1385
|
-
? await progressiveDispatch(sessionName, normalizedAssignments, {
|
|
1386
|
-
layout,
|
|
1387
|
-
safeProgress,
|
|
1388
|
-
dashboardLayout,
|
|
1389
|
-
sessionOwnership,
|
|
1390
|
-
_deps: deps,
|
|
1391
|
-
})
|
|
1392
|
-
: await batchDispatch(sessionName, normalizedAssignments, {
|
|
1393
|
-
layout,
|
|
1394
|
-
safeProgress,
|
|
1395
|
-
dashboardLayout,
|
|
1396
|
-
sessionOwnership,
|
|
1397
|
-
_deps: deps,
|
|
1398
|
-
});
|
|
1399
|
-
if (nativeBridge && nativeBridgeMode === "agents") {
|
|
1400
|
-
daemonDispatches = dispatches;
|
|
1401
|
-
const createObservationSession =
|
|
1402
|
-
deps.createDaemonObservationSession || createDaemonObservationSession;
|
|
1403
|
-
let observation = null;
|
|
1404
|
-
try {
|
|
1405
|
-
observation = createObservationSession(sessionName, dispatches, {
|
|
1406
|
-
autoAttach,
|
|
1407
|
-
dashboard,
|
|
1019
|
+
const dispatches = progressive
|
|
1020
|
+
? await progressiveDispatch(sessionName, normalizedAssignments, {
|
|
1021
|
+
layout,
|
|
1022
|
+
safeProgress,
|
|
1408
1023
|
dashboardLayout,
|
|
1409
|
-
|
|
1410
|
-
leadTmux,
|
|
1411
|
-
onProgress: safeProgress,
|
|
1024
|
+
sessionOwnership,
|
|
1412
1025
|
_deps: deps,
|
|
1413
|
-
})
|
|
1414
|
-
|
|
1415
|
-
|
|
1026
|
+
})
|
|
1027
|
+
: await batchDispatch(sessionName, normalizedAssignments, {
|
|
1028
|
+
layout,
|
|
1416
1029
|
safeProgress,
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
"observer setup failed",
|
|
1421
|
-
);
|
|
1422
|
-
}
|
|
1423
|
-
if (observation) {
|
|
1424
|
-
sessionOwnership.acquire(observation.sessionName);
|
|
1425
|
-
safeProgress({
|
|
1426
|
-
type: "session_created",
|
|
1427
|
-
sessionName: observation.sessionName,
|
|
1428
|
-
panes: observation.panes,
|
|
1429
|
-
dashboardLayout: observation.dashboardLayout,
|
|
1430
|
-
observationSource: "daemon-files",
|
|
1030
|
+
dashboardLayout,
|
|
1031
|
+
sessionOwnership,
|
|
1032
|
+
_deps: deps,
|
|
1431
1033
|
});
|
|
1432
|
-
|
|
1034
|
+
if (nativeBridge && nativeBridgeMode === "agents") {
|
|
1035
|
+
await openHeadlessAgentsRows(sessionName, dispatches, agentsRows, {
|
|
1036
|
+
safeProgress,
|
|
1037
|
+
_deps: deps,
|
|
1038
|
+
});
|
|
1433
1039
|
}
|
|
1434
1040
|
|
|
1435
|
-
const results =
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
)
|
|
1444
|
-
: await awaitAll(
|
|
1445
|
-
sessionName,
|
|
1446
|
-
dispatches,
|
|
1447
|
-
timeoutSec,
|
|
1448
|
-
safeProgress,
|
|
1449
|
-
progressIntervalSec,
|
|
1450
|
-
lifecycle,
|
|
1451
|
-
);
|
|
1041
|
+
const results = await (deps.awaitAll || awaitAll)(
|
|
1042
|
+
sessionName,
|
|
1043
|
+
dispatches,
|
|
1044
|
+
timeoutSec,
|
|
1045
|
+
safeProgress,
|
|
1046
|
+
progressIntervalSec,
|
|
1047
|
+
lifecycle,
|
|
1048
|
+
);
|
|
1452
1049
|
const collected = await collectResults(results);
|
|
1453
1050
|
|
|
1454
1051
|
// 완료 시 TUI에 최종 상태 반영 후 닫기
|
|
@@ -1481,21 +1078,17 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1481
1078
|
runError = error;
|
|
1482
1079
|
}
|
|
1483
1080
|
|
|
1484
|
-
if (!runCompleted)
|
|
1485
|
-
try {
|
|
1486
|
-
if (daemonDispatches.length > 0) {
|
|
1487
|
-
await cleanupDaemonDispatches(daemonDispatches);
|
|
1488
|
-
}
|
|
1489
|
-
} catch (error) {
|
|
1081
|
+
if (!runCompleted) {
|
|
1490
1082
|
sessionOwnership.release();
|
|
1491
|
-
|
|
1492
|
-
runFailed = true;
|
|
1493
|
-
runError = error;
|
|
1494
|
-
}
|
|
1083
|
+
await agentsRows.release();
|
|
1495
1084
|
}
|
|
1496
|
-
|
|
1497
1085
|
if (runFailed) throw runError;
|
|
1498
|
-
return {
|
|
1086
|
+
return {
|
|
1087
|
+
sessionName,
|
|
1088
|
+
results: completedResults,
|
|
1089
|
+
sessionOwnership,
|
|
1090
|
+
agentsRows,
|
|
1091
|
+
};
|
|
1499
1092
|
}
|
|
1500
1093
|
|
|
1501
1094
|
/**
|
|
@@ -1514,13 +1107,16 @@ export async function runHeadlessWithCleanup(assignments, opts = {}) {
|
|
|
1514
1107
|
const sessionOwnership = createHeadlessSessionOwnership(
|
|
1515
1108
|
deps.killPsmuxSession || killPsmuxSession,
|
|
1516
1109
|
);
|
|
1110
|
+
const agentsRows = createAgentsRowOwnership();
|
|
1517
1111
|
|
|
1518
1112
|
try {
|
|
1519
1113
|
return await run(sessionName, assignments, {
|
|
1520
1114
|
...runOpts,
|
|
1521
1115
|
_sessionOwnership: sessionOwnership,
|
|
1116
|
+
_agentsRows: agentsRows,
|
|
1522
1117
|
});
|
|
1523
1118
|
} finally {
|
|
1119
|
+
await agentsRows.release();
|
|
1524
1120
|
sessionOwnership.release();
|
|
1525
1121
|
// WT split pane은 psmux 종료 시 셸이 끝나면서 자동으로 닫힘
|
|
1526
1122
|
// 수동 close-pane 불필요 (레이스 컨디션으로 WT 에러 발생)
|
|
@@ -1603,98 +1199,52 @@ function emitObserverWarning(
|
|
|
1603
1199
|
});
|
|
1604
1200
|
}
|
|
1605
1201
|
|
|
1606
|
-
// ─── v6.0.0: Lead-Direct Interactive Mode ───
|
|
1607
|
-
|
|
1608
1202
|
/**
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
* @param {string} sessionName
|
|
1613
|
-
* @param {Array<{resultFile:string}>} dispatches
|
|
1614
|
-
* @param {object} [opts]
|
|
1615
|
-
* @returns {{sessionName:string, panes:string[], dashboardLayout:string}|null}
|
|
1203
|
+
* 워커 pane 마다 `claude agents` 행을 연다. 행은 그 pane 을 읽기 전용으로 보여 준다.
|
|
1204
|
+
* 행을 못 열어도 워커는 tmux 에서 계속 돈다. 실패는 observer_warning 으로 알린다.
|
|
1616
1205
|
*/
|
|
1617
|
-
|
|
1206
|
+
async function openHeadlessAgentsRows(
|
|
1618
1207
|
sessionName,
|
|
1619
1208
|
dispatches,
|
|
1209
|
+
rows,
|
|
1620
1210
|
opts = {},
|
|
1621
1211
|
) {
|
|
1622
1212
|
const deps = opts._deps || {};
|
|
1623
1213
|
const platform = deps.platform ?? process.platform;
|
|
1624
|
-
|
|
1625
|
-
const leadTmux = String(opts.leadTmux || "");
|
|
1626
|
-
if (!opts.autoAttach) return null;
|
|
1627
|
-
if (platform !== "darwin" && platform !== "linux") return null;
|
|
1628
|
-
if (!leadTmux.trim()) return null;
|
|
1629
|
-
if (!/^%\d+$/u.test(leadPane)) return null;
|
|
1630
|
-
if (!Array.isArray(dispatches) || dispatches.length === 0) return null;
|
|
1631
|
-
|
|
1632
|
-
const safeSessionName = sanitizeSessionName(sessionName);
|
|
1633
|
-
const resolvedLayout = opts.dashboard
|
|
1634
|
-
? resolveDashboardLayout(opts.dashboardLayout, dispatches.length)
|
|
1635
|
-
: "lite";
|
|
1636
|
-
const createSession = deps.createPsmuxSession || createPsmuxSession;
|
|
1637
|
-
const applyTheme = deps.applyTrifluxTheme || applyTrifluxTheme;
|
|
1638
|
-
const sendKeys = deps.sendKeysToPane || sendKeysToPane;
|
|
1639
|
-
const killSession = deps.killPsmuxSession || killPsmuxSession;
|
|
1640
|
-
let created = false;
|
|
1641
|
-
let stage = "observer_session_create";
|
|
1642
|
-
|
|
1643
|
-
try {
|
|
1644
|
-
const session = createSession(safeSessionName, {
|
|
1645
|
-
layout: "2x2",
|
|
1646
|
-
paneCount: 1,
|
|
1647
|
-
});
|
|
1648
|
-
created = true;
|
|
1649
|
-
stage = "observer_theme";
|
|
1650
|
-
applyTheme(safeSessionName);
|
|
1651
|
-
stage = "observer_viewer_start";
|
|
1652
|
-
const targetPane = session.panes?.[0];
|
|
1653
|
-
if (!targetPane) throw new Error("observer pane was not created");
|
|
1654
|
-
|
|
1655
|
-
const viewerCommand = [
|
|
1656
|
-
"exec",
|
|
1657
|
-
shellQuote(process.execPath),
|
|
1658
|
-
shellQuote(join(SCRIPT_DIR, "tui-viewer.mjs")),
|
|
1659
|
-
"--session",
|
|
1660
|
-
shellQuote(safeSessionName),
|
|
1661
|
-
"--result-dir",
|
|
1662
|
-
shellQuote(RESULT_DIR),
|
|
1663
|
-
"--layout",
|
|
1664
|
-
shellQuote(resolvedLayout),
|
|
1665
|
-
"--source",
|
|
1666
|
-
"files",
|
|
1667
|
-
"--workers",
|
|
1668
|
-
String(dispatches.length),
|
|
1669
|
-
"--lead-pane",
|
|
1670
|
-
shellQuote(leadPane),
|
|
1671
|
-
].join(" ");
|
|
1672
|
-
sendKeys(targetPane, viewerCommand, true);
|
|
1673
|
-
|
|
1674
|
-
return {
|
|
1675
|
-
sessionName: safeSessionName,
|
|
1676
|
-
panes: session.panes,
|
|
1677
|
-
dashboardLayout: resolvedLayout,
|
|
1678
|
-
};
|
|
1679
|
-
} catch (error) {
|
|
1680
|
-
if (created) {
|
|
1681
|
-
try {
|
|
1682
|
-
killSession(safeSessionName);
|
|
1683
|
-
} catch {
|
|
1684
|
-
/* fail-open: observer failure must not stop daemon workers */
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1214
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
1687
1215
|
emitObserverWarning(
|
|
1688
|
-
opts.
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1216
|
+
opts.safeProgress,
|
|
1217
|
+
sessionName,
|
|
1218
|
+
"agents_row",
|
|
1219
|
+
null,
|
|
1220
|
+
`claude agents rows need tmux on macOS/Linux (platform: ${platform})`,
|
|
1693
1221
|
);
|
|
1694
|
-
return
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const open = deps.openAgentsRow || openAgentsRow;
|
|
1225
|
+
for (const d of dispatches) {
|
|
1226
|
+
try {
|
|
1227
|
+
const row = await open({
|
|
1228
|
+
name: `${d.cli} ${d.displayName || d.paneName}`,
|
|
1229
|
+
target: d.paneId,
|
|
1230
|
+
cwd: d.cwd || process.cwd(),
|
|
1231
|
+
});
|
|
1232
|
+
rows.add(row.short);
|
|
1233
|
+
d.agentsRowShort = row.short;
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
emitObserverWarning(
|
|
1236
|
+
opts.safeProgress,
|
|
1237
|
+
sessionName,
|
|
1238
|
+
"agents_row",
|
|
1239
|
+
error,
|
|
1240
|
+
`claude agents row failed for ${d.paneName}`,
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1695
1243
|
}
|
|
1696
1244
|
}
|
|
1697
1245
|
|
|
1246
|
+
// ─── v6.0.0: Lead-Direct Interactive Mode ───
|
|
1247
|
+
|
|
1698
1248
|
/**
|
|
1699
1249
|
* attached tmux의 현재 leader pane 옆에 headless session view를 연다.
|
|
1700
1250
|
* 새 pane의 attach client는 headless session 종료와 함께 자연스럽게 종료된다.
|
|
@@ -2054,11 +1604,13 @@ export async function runHeadlessInteractive(
|
|
|
2054
1604
|
let sessionOwnership = createHeadlessSessionOwnership(
|
|
2055
1605
|
deps.killPsmuxSession || killPsmuxSession,
|
|
2056
1606
|
);
|
|
1607
|
+
const agentsRows = createAgentsRowOwnership();
|
|
2057
1608
|
|
|
2058
1609
|
// Phase 1: 세션 생성 → 즉시 관찰 pane/tab attach → dispatch → 대기 → 결과 수집
|
|
2059
1610
|
const runResult = await run(sessionName, assignments, {
|
|
2060
1611
|
...interactiveRunOpts,
|
|
2061
1612
|
_sessionOwnership: sessionOwnership,
|
|
1613
|
+
_agentsRows: agentsRows,
|
|
2062
1614
|
});
|
|
2063
1615
|
const { results } = runResult;
|
|
2064
1616
|
sessionOwnership = runResult.sessionOwnership || sessionOwnership;
|
|
@@ -2141,6 +1693,7 @@ export async function runHeadlessInteractive(
|
|
|
2141
1693
|
kill() {
|
|
2142
1694
|
if (this._killed) return;
|
|
2143
1695
|
this._killed = true;
|
|
1696
|
+
void agentsRows.release();
|
|
2144
1697
|
sessionOwnership.release();
|
|
2145
1698
|
// attach pane/tab은 psmux 종료 → attach client 종료 → 자동 닫힘
|
|
2146
1699
|
// 수동 close-pane 불필요 (레이스 컨디션으로 WT 0x80070002 에러 발생)
|