@triflux/remote 10.51.0 → 10.53.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/claude-transcript.mjs +19 -0
- 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-headless.mjs +2 -0
- 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 +132 -551
- package/hub/team/psmux.mjs +3 -33
- package/hub/team/results-index.mjs +92 -0
- 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 +20 -599
- 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 +42 -101
- 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,15 +40,14 @@ import {
|
|
|
52
40
|
killPsmuxSession,
|
|
53
41
|
psmuxExec,
|
|
54
42
|
psmuxSessionExists,
|
|
55
|
-
sendKeysToPane,
|
|
56
43
|
startCapture,
|
|
57
44
|
waitForCompletion,
|
|
58
45
|
} from "./psmux.mjs";
|
|
46
|
+
import { createResultsIndex, resultsIndexPath } from "./results-index.mjs";
|
|
59
47
|
import { createLogDashboard } from "./tui.mjs";
|
|
60
48
|
import { createWtManager } from "./wt-manager.mjs";
|
|
61
49
|
|
|
62
50
|
const RESULT_DIR = join(tmpdir(), "tfx-headless");
|
|
63
|
-
const DAEMON_COMPLETION_PREFIX = "__TFX_HEADLESS_DONE__:";
|
|
64
51
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
65
52
|
|
|
66
53
|
/** CLI별 브랜드 — 이모지 + 공식 색상 (HUD와 통일) */
|
|
@@ -140,6 +127,14 @@ export function normalizeHeadlessRole(role) {
|
|
|
140
127
|
return normalized && VALID_ROUTE_AGENTS.has(normalized) ? normalized : "";
|
|
141
128
|
}
|
|
142
129
|
|
|
130
|
+
export function headlessResultFile(sessionName, paneName) {
|
|
131
|
+
return join(RESULT_DIR, `${sessionName}-${paneName}.txt`).replace(/\\/g, "/");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function headlessResultsIndexPath(sessionName) {
|
|
135
|
+
return resultsIndexPath(RESULT_DIR, sessionName).replace(/\\/g, "/");
|
|
136
|
+
}
|
|
137
|
+
|
|
143
138
|
export function resolveHeadlessDisplayName(assignment = {}, paneName = "") {
|
|
144
139
|
const normalized = String(assignment.displayName || "")
|
|
145
140
|
.trim()
|
|
@@ -235,7 +230,11 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
235
230
|
let ctx = readFileSync(contextFile, "utf8");
|
|
236
231
|
let truncated = false;
|
|
237
232
|
if (Buffer.byteLength(ctx, "utf8") > 32768) {
|
|
238
|
-
|
|
233
|
+
// 바이트 경계에서 잘린 다바이트 글자는 U+FFFD 로 디코딩되므로 떼어 낸다.
|
|
234
|
+
ctx = Buffer.from(ctx)
|
|
235
|
+
.subarray(0, 32768)
|
|
236
|
+
.toString("utf8")
|
|
237
|
+
.replace(/�+$/u, "");
|
|
239
238
|
truncated = true;
|
|
240
239
|
}
|
|
241
240
|
if (truncated) {
|
|
@@ -309,48 +308,6 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
309
308
|
return `cd '${safeCwd.replace(/'/g, "'\\''")}' && ${backendCommand}`;
|
|
310
309
|
}
|
|
311
310
|
|
|
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
311
|
/**
|
|
355
312
|
* 이전 run 의 stale .txt / .partial / .err 를 제거한다.
|
|
356
313
|
* Issue #118 Codex review R1 HIGH: resultFile 경로 재사용 시 (워커 restart 또는
|
|
@@ -413,43 +370,28 @@ function createHeadlessIntervention(dispatch, fallback) {
|
|
|
413
370
|
if (typeof fallback === "function")
|
|
414
371
|
return (await fallback(context)) === true;
|
|
415
372
|
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
return `${capturePsmuxPane(context.paneId || dispatch.paneId, 50)}\0${mtime}`;
|
|
427
|
-
};
|
|
373
|
+
const readActivitySignature = () => {
|
|
374
|
+
let mtime = 0;
|
|
375
|
+
try {
|
|
376
|
+
mtime = statSync(dispatch.resultFile).mtimeMs;
|
|
377
|
+
} catch {
|
|
378
|
+
/* result file has not been written yet */
|
|
379
|
+
}
|
|
380
|
+
return `${capturePsmuxPane(context.paneId || dispatch.paneId, 50)}\0${mtime}`;
|
|
381
|
+
};
|
|
428
382
|
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
|
-
},
|
|
383
|
+
target: {
|
|
384
|
+
channel: "tmux-pane",
|
|
385
|
+
paneId: context.paneId || dispatch.paneId,
|
|
386
|
+
cli: dispatch.cli,
|
|
387
|
+
sessionId: dispatch.sessionId,
|
|
388
|
+
},
|
|
447
389
|
readActivitySignature,
|
|
448
390
|
deps: {
|
|
449
391
|
// 이 트랙이 재실행 소유자다. pane 채널은 기존 completion wrapper를 다시
|
|
450
392
|
// dispatch하여 token/result 계약을 보존한다.
|
|
451
393
|
resumeHandler: async () => {
|
|
452
|
-
if (
|
|
394
|
+
if (!dispatch.command) return { ok: false };
|
|
453
395
|
const restarted = dispatchCommand(
|
|
454
396
|
context.sessionName,
|
|
455
397
|
context.paneId || dispatch.paneId,
|
|
@@ -610,10 +552,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
610
552
|
});
|
|
611
553
|
|
|
612
554
|
// 캡처 시작 + 컬러 배너 + 명령 dispatch
|
|
613
|
-
const resultFile =
|
|
614
|
-
RESULT_DIR,
|
|
615
|
-
`${sessionName}-${paneName}.txt`,
|
|
616
|
-
).replace(/\\/g, "/");
|
|
555
|
+
const resultFile = headlessResultFile(sessionName, paneName);
|
|
617
556
|
// Issue #118 review R1 HIGH: stale artifact 제거 (이전 run / restart 잔재)
|
|
618
557
|
cleanStaleResultArtifacts(resultFile);
|
|
619
558
|
const cmd = buildHeadlessCommand(
|
|
@@ -701,10 +640,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
701
640
|
const displayName = resolveHeadlessDisplayName(assignment, paneName);
|
|
702
641
|
const workerId = getHeadlessWorkerAgentId(sessionName, i);
|
|
703
642
|
const resolvedCli = resolveCliType(assignment.cli);
|
|
704
|
-
const resultFile =
|
|
705
|
-
RESULT_DIR,
|
|
706
|
-
`${sessionName}-${paneName}.txt`,
|
|
707
|
-
).replace(/\\/g, "/");
|
|
643
|
+
const resultFile = headlessResultFile(sessionName, paneName);
|
|
708
644
|
// Issue #118 review R1 HIGH: stale artifact 제거 (이전 run / restart 잔재)
|
|
709
645
|
cleanStaleResultArtifacts(resultFile);
|
|
710
646
|
const cmd = buildHeadlessCommand(
|
|
@@ -752,133 +688,6 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
752
688
|
);
|
|
753
689
|
}
|
|
754
690
|
|
|
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
691
|
/**
|
|
883
692
|
* 모든 dispatch를 병렬 대기하며 완료 결과를 수집한다.
|
|
884
693
|
* @param {string} sessionName
|
|
@@ -998,169 +807,6 @@ async function awaitAll(
|
|
|
998
807
|
);
|
|
999
808
|
}
|
|
1000
809
|
|
|
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
810
|
/**
|
|
1165
811
|
* git diff + handoff 파이프라인을 적용하여 최종 결과 배열을 반환한다.
|
|
1166
812
|
* @param {Array<{d, completion, output}>} results
|
|
@@ -1243,20 +889,18 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1243
889
|
const sessionOwnership =
|
|
1244
890
|
opts._sessionOwnership ||
|
|
1245
891
|
createHeadlessSessionOwnership(deps.killPsmuxSession || killPsmuxSession);
|
|
892
|
+
const agentsRows = opts._agentsRows || createAgentsRowOwnership();
|
|
1246
893
|
let {
|
|
1247
894
|
timeoutSec = 900,
|
|
1248
895
|
layout = "2x2",
|
|
1249
896
|
onProgress,
|
|
1250
897
|
progressIntervalSec = 0,
|
|
1251
898
|
progressive = true,
|
|
1252
|
-
autoAttach = false,
|
|
1253
899
|
dashboard = false,
|
|
1254
900
|
dashboardLayout = "single",
|
|
1255
901
|
nativeBridge = false,
|
|
1256
902
|
nativeBridgeMode = "agents",
|
|
1257
903
|
onIntervene,
|
|
1258
|
-
leadPane = null,
|
|
1259
|
-
leadTmux = null,
|
|
1260
904
|
} = opts;
|
|
1261
905
|
if (!Number.isFinite(Number(timeoutSec)) || Number(timeoutSec) <= 0)
|
|
1262
906
|
timeoutSec = 900;
|
|
@@ -1274,10 +918,26 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1274
918
|
);
|
|
1275
919
|
|
|
1276
920
|
if (normalizedAssignments.length === 0) {
|
|
1277
|
-
return { sessionName, results: [], sessionOwnership };
|
|
921
|
+
return { sessionName, results: [], sessionOwnership, agentsRows };
|
|
1278
922
|
}
|
|
1279
923
|
|
|
1280
|
-
|
|
924
|
+
const resultsIndex = createResultsIndex(
|
|
925
|
+
headlessResultsIndexPath(sessionName),
|
|
926
|
+
sessionName,
|
|
927
|
+
normalizedAssignments.map((a, i) => {
|
|
928
|
+
const paneName = `worker-${i + 1}`;
|
|
929
|
+
return {
|
|
930
|
+
paneName,
|
|
931
|
+
displayName: a.displayName,
|
|
932
|
+
cli: resolveCliType(a.cli),
|
|
933
|
+
role: a.role,
|
|
934
|
+
resultFile: headlessResultFile(sessionName, paneName),
|
|
935
|
+
};
|
|
936
|
+
}),
|
|
937
|
+
// 같은 이름의 남의 세션 색인을 덮어쓰지 않도록 세션을 소유한 동안만 쓴다.
|
|
938
|
+
() => sessionOwnership.owned,
|
|
939
|
+
);
|
|
940
|
+
|
|
1281
941
|
let runCompleted = false;
|
|
1282
942
|
let completedResults = [];
|
|
1283
943
|
let runFailed = false;
|
|
@@ -1356,6 +1016,11 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1356
1016
|
|
|
1357
1017
|
// onProgress 예외를 삼켜 실행 흐름 보호 (onPoll과 동일 패턴)
|
|
1358
1018
|
const combinedProgress = (event) => {
|
|
1019
|
+
if (event.type === "session_created") resultsIndex.begin();
|
|
1020
|
+
else if (event.type === "dispatched")
|
|
1021
|
+
resultsIndex.workerStarted(event.paneName);
|
|
1022
|
+
else if (event.type === "completed")
|
|
1023
|
+
resultsIndex.workerFinished(event.paneName, event);
|
|
1359
1024
|
feedTui(event);
|
|
1360
1025
|
if (onProgress) {
|
|
1361
1026
|
try {
|
|
@@ -1376,79 +1041,36 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1376
1041
|
try {
|
|
1377
1042
|
const progressiveDispatch = deps.dispatchProgressive || dispatchProgressive;
|
|
1378
1043
|
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,
|
|
1044
|
+
const dispatches = progressive
|
|
1045
|
+
? await progressiveDispatch(sessionName, normalizedAssignments, {
|
|
1046
|
+
layout,
|
|
1047
|
+
safeProgress,
|
|
1408
1048
|
dashboardLayout,
|
|
1409
|
-
|
|
1410
|
-
leadTmux,
|
|
1411
|
-
onProgress: safeProgress,
|
|
1049
|
+
sessionOwnership,
|
|
1412
1050
|
_deps: deps,
|
|
1413
|
-
})
|
|
1414
|
-
|
|
1415
|
-
|
|
1051
|
+
})
|
|
1052
|
+
: await batchDispatch(sessionName, normalizedAssignments, {
|
|
1053
|
+
layout,
|
|
1416
1054
|
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",
|
|
1055
|
+
dashboardLayout,
|
|
1056
|
+
sessionOwnership,
|
|
1057
|
+
_deps: deps,
|
|
1431
1058
|
});
|
|
1432
|
-
|
|
1059
|
+
if (nativeBridge && nativeBridgeMode === "agents") {
|
|
1060
|
+
await openHeadlessAgentsRows(sessionName, dispatches, agentsRows, {
|
|
1061
|
+
safeProgress,
|
|
1062
|
+
_deps: deps,
|
|
1063
|
+
});
|
|
1433
1064
|
}
|
|
1434
1065
|
|
|
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
|
-
);
|
|
1066
|
+
const results = await (deps.awaitAll || awaitAll)(
|
|
1067
|
+
sessionName,
|
|
1068
|
+
dispatches,
|
|
1069
|
+
timeoutSec,
|
|
1070
|
+
safeProgress,
|
|
1071
|
+
progressIntervalSec,
|
|
1072
|
+
lifecycle,
|
|
1073
|
+
);
|
|
1452
1074
|
const collected = await collectResults(results);
|
|
1453
1075
|
|
|
1454
1076
|
// 완료 시 TUI에 최종 상태 반영 후 닫기
|
|
@@ -1480,22 +1102,20 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1480
1102
|
runFailed = true;
|
|
1481
1103
|
runError = error;
|
|
1482
1104
|
}
|
|
1105
|
+
resultsIndex.finish(runCompleted);
|
|
1483
1106
|
|
|
1484
|
-
if (!runCompleted)
|
|
1485
|
-
try {
|
|
1486
|
-
if (daemonDispatches.length > 0) {
|
|
1487
|
-
await cleanupDaemonDispatches(daemonDispatches);
|
|
1488
|
-
}
|
|
1489
|
-
} catch (error) {
|
|
1107
|
+
if (!runCompleted) {
|
|
1490
1108
|
sessionOwnership.release();
|
|
1491
|
-
|
|
1492
|
-
runFailed = true;
|
|
1493
|
-
runError = error;
|
|
1494
|
-
}
|
|
1109
|
+
await agentsRows.release();
|
|
1495
1110
|
}
|
|
1496
|
-
|
|
1497
1111
|
if (runFailed) throw runError;
|
|
1498
|
-
return {
|
|
1112
|
+
return {
|
|
1113
|
+
sessionName,
|
|
1114
|
+
results: completedResults,
|
|
1115
|
+
resultsIndexPath: resultsIndex.path,
|
|
1116
|
+
sessionOwnership,
|
|
1117
|
+
agentsRows,
|
|
1118
|
+
};
|
|
1499
1119
|
}
|
|
1500
1120
|
|
|
1501
1121
|
/**
|
|
@@ -1514,13 +1134,16 @@ export async function runHeadlessWithCleanup(assignments, opts = {}) {
|
|
|
1514
1134
|
const sessionOwnership = createHeadlessSessionOwnership(
|
|
1515
1135
|
deps.killPsmuxSession || killPsmuxSession,
|
|
1516
1136
|
);
|
|
1137
|
+
const agentsRows = createAgentsRowOwnership();
|
|
1517
1138
|
|
|
1518
1139
|
try {
|
|
1519
1140
|
return await run(sessionName, assignments, {
|
|
1520
1141
|
...runOpts,
|
|
1521
1142
|
_sessionOwnership: sessionOwnership,
|
|
1143
|
+
_agentsRows: agentsRows,
|
|
1522
1144
|
});
|
|
1523
1145
|
} finally {
|
|
1146
|
+
await agentsRows.release();
|
|
1524
1147
|
sessionOwnership.release();
|
|
1525
1148
|
// WT split pane은 psmux 종료 시 셸이 끝나면서 자동으로 닫힘
|
|
1526
1149
|
// 수동 close-pane 불필요 (레이스 컨디션으로 WT 에러 발생)
|
|
@@ -1603,98 +1226,52 @@ function emitObserverWarning(
|
|
|
1603
1226
|
});
|
|
1604
1227
|
}
|
|
1605
1228
|
|
|
1606
|
-
// ─── v6.0.0: Lead-Direct Interactive Mode ───
|
|
1607
|
-
|
|
1608
1229
|
/**
|
|
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}
|
|
1230
|
+
* 워커 pane 마다 `claude agents` 행을 연다. 행은 그 pane 을 읽기 전용으로 보여 준다.
|
|
1231
|
+
* 행을 못 열어도 워커는 tmux 에서 계속 돈다. 실패는 observer_warning 으로 알린다.
|
|
1616
1232
|
*/
|
|
1617
|
-
|
|
1233
|
+
async function openHeadlessAgentsRows(
|
|
1618
1234
|
sessionName,
|
|
1619
1235
|
dispatches,
|
|
1236
|
+
rows,
|
|
1620
1237
|
opts = {},
|
|
1621
1238
|
) {
|
|
1622
1239
|
const deps = opts._deps || {};
|
|
1623
1240
|
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
|
-
}
|
|
1241
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
1687
1242
|
emitObserverWarning(
|
|
1688
|
-
opts.
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1243
|
+
opts.safeProgress,
|
|
1244
|
+
sessionName,
|
|
1245
|
+
"agents_row",
|
|
1246
|
+
null,
|
|
1247
|
+
`claude agents rows need tmux on macOS/Linux (platform: ${platform})`,
|
|
1693
1248
|
);
|
|
1694
|
-
return
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const open = deps.openAgentsRow || openAgentsRow;
|
|
1252
|
+
for (const d of dispatches) {
|
|
1253
|
+
try {
|
|
1254
|
+
const row = await open({
|
|
1255
|
+
name: `${d.cli} ${d.displayName || d.paneName}`,
|
|
1256
|
+
target: d.paneId,
|
|
1257
|
+
cwd: d.cwd || process.cwd(),
|
|
1258
|
+
});
|
|
1259
|
+
rows.add(row.short);
|
|
1260
|
+
d.agentsRowShort = row.short;
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
emitObserverWarning(
|
|
1263
|
+
opts.safeProgress,
|
|
1264
|
+
sessionName,
|
|
1265
|
+
"agents_row",
|
|
1266
|
+
error,
|
|
1267
|
+
`claude agents row failed for ${d.paneName}`,
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1695
1270
|
}
|
|
1696
1271
|
}
|
|
1697
1272
|
|
|
1273
|
+
// ─── v6.0.0: Lead-Direct Interactive Mode ───
|
|
1274
|
+
|
|
1698
1275
|
/**
|
|
1699
1276
|
* attached tmux의 현재 leader pane 옆에 headless session view를 연다.
|
|
1700
1277
|
* 새 pane의 attach client는 headless session 종료와 함께 자연스럽게 종료된다.
|
|
@@ -2054,11 +1631,13 @@ export async function runHeadlessInteractive(
|
|
|
2054
1631
|
let sessionOwnership = createHeadlessSessionOwnership(
|
|
2055
1632
|
deps.killPsmuxSession || killPsmuxSession,
|
|
2056
1633
|
);
|
|
1634
|
+
const agentsRows = createAgentsRowOwnership();
|
|
2057
1635
|
|
|
2058
1636
|
// Phase 1: 세션 생성 → 즉시 관찰 pane/tab attach → dispatch → 대기 → 결과 수집
|
|
2059
1637
|
const runResult = await run(sessionName, assignments, {
|
|
2060
1638
|
...interactiveRunOpts,
|
|
2061
1639
|
_sessionOwnership: sessionOwnership,
|
|
1640
|
+
_agentsRows: agentsRows,
|
|
2062
1641
|
});
|
|
2063
1642
|
const { results } = runResult;
|
|
2064
1643
|
sessionOwnership = runResult.sessionOwnership || sessionOwnership;
|
|
@@ -2086,6 +1665,7 @@ export async function runHeadlessInteractive(
|
|
|
2086
1665
|
const handle = {
|
|
2087
1666
|
sessionName,
|
|
2088
1667
|
results,
|
|
1668
|
+
resultsIndexPath: runResult.resultsIndexPath,
|
|
2089
1669
|
dispatches,
|
|
2090
1670
|
_killed: false,
|
|
2091
1671
|
|
|
@@ -2141,6 +1721,7 @@ export async function runHeadlessInteractive(
|
|
|
2141
1721
|
kill() {
|
|
2142
1722
|
if (this._killed) return;
|
|
2143
1723
|
this._killed = true;
|
|
1724
|
+
void agentsRows.release();
|
|
2144
1725
|
sessionOwnership.release();
|
|
2145
1726
|
// attach pane/tab은 psmux 종료 → attach client 종료 → 자동 닫힘
|
|
2146
1727
|
// 수동 close-pane 불필요 (레이스 컨디션으로 WT 0x80070002 에러 발생)
|