@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.
Files changed (59) hide show
  1. package/cto/brief.mjs +0 -8
  2. package/cto/collect.mjs +1 -127
  3. package/cto/current.schema.json +2 -19
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -139
  7. package/hub/index.mjs +0 -9
  8. package/hub/team/agents-row-attach.py +120 -0
  9. package/hub/team/agents-row.mjs +200 -0
  10. package/hub/team/claude-daemon-control.mjs +1 -2
  11. package/hub/team/claude-session-projection.mjs +5 -73
  12. package/hub/team/cli/commands/debug.mjs +1 -2
  13. package/hub/team/cli/commands/start/index.mjs +2 -1
  14. package/hub/team/cli/commands/start/start-mux.mjs +2 -4
  15. package/hub/team/cli/services/state-store.mjs +5 -20
  16. package/hub/team/headless.mjs +96 -543
  17. package/hub/team/psmux.mjs +3 -33
  18. package/hub/team/session-context.mjs +1 -1
  19. package/hub/team/tui-viewer.mjs +9 -56
  20. package/hub/team/tui.mjs +0 -37
  21. package/hub/team/uds-orchestrator.mjs +2 -580
  22. package/hub/workers/factory.mjs +6 -51
  23. package/hub/workers/interface.mjs +1 -7
  24. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  25. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
  26. package/package.json +3 -8
  27. package/scripts/lib/agent-route-policy.mjs +0 -67
  28. package/scripts/lib/cli-claude.mjs +0 -12
  29. package/scripts/lib/env-probe.mjs +0 -70
  30. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  31. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  32. package/hub/hub-lifecycle.mjs +0 -248
  33. package/hub/pipe.mjs +0 -825
  34. package/hub/promote-penalties.mjs +0 -138
  35. package/hub/public/dashboard.html +0 -416
  36. package/hub/schema.sql +0 -209
  37. package/hub/server.mjs +0 -3048
  38. package/hub/store-adapter.mjs +0 -182
  39. package/hub/store.mjs +0 -1736
  40. package/hub/team/cli/services/hub-client.mjs +0 -178
  41. package/hub/team/conductor-registry.mjs +0 -121
  42. package/hub/team/cto-auto-collect.mjs +0 -115
  43. package/hub/team/lead-control.mjs +0 -191
  44. package/hub/team/session-sync.mjs +0 -316
  45. package/hub/team/swarm-intent.mjs +0 -18
  46. package/hub/team/synapse-cli.mjs +0 -458
  47. package/hub/team/synapse-http.mjs +0 -311
  48. package/hub/team/synapse-registry.mjs +0 -618
  49. package/hub/team/tui-synapse.mjs +0 -223
  50. package/hub/team/worker-sandbox.mjs +0 -125
  51. package/hub/tools.mjs +0 -900
  52. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  53. package/hub/workers/delegator-mcp.mjs +0 -1098
  54. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  55. package/scripts/lib/claudemd-scanner.mjs +0 -26
  56. package/scripts/lib/context.mjs +0 -51
  57. package/scripts/lib/handoff.mjs +0 -187
  58. package/scripts/lib/logger.mjs +0 -122
  59. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -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
- ctx = Buffer.from(ctx).subarray(0, 32768).toString("utf8");
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 isDaemon = context.channel === "daemon";
417
- const readActivitySignature = isDaemon
418
- ? createFileActivitySource({ files: [dispatch.resultFile] })
419
- : () => {
420
- let mtime = 0;
421
- try {
422
- mtime = statSync(dispatch.resultFile).mtimeMs;
423
- } catch {
424
- /* result file has not been written yet */
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: isDaemon
430
- ? {
431
- channel: "claude-daemon",
432
- cli: dispatch.cli || "claude",
433
- sessionId: dispatch.sessionId,
434
- daemon: {
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 (isDaemon || !dispatch.command) return { ok: false };
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
- nativeBridge && nativeBridgeMode === "agents"
1381
- ? await dispatchDaemonBatch(sessionName, normalizedAssignments, {
1382
- safeProgress,
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
- leadPane,
1410
- leadTmux,
1411
- onProgress: safeProgress,
1024
+ sessionOwnership,
1412
1025
  _deps: deps,
1413
- });
1414
- } catch (error) {
1415
- emitObserverWarning(
1026
+ })
1027
+ : await batchDispatch(sessionName, normalizedAssignments, {
1028
+ layout,
1416
1029
  safeProgress,
1417
- sessionName,
1418
- "observer_session_create",
1419
- error,
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
- nativeBridge && nativeBridgeMode === "agents"
1437
- ? await awaitAllDaemon(
1438
- dispatches,
1439
- timeoutSec,
1440
- safeProgress,
1441
- progressIntervalSec,
1442
- lifecycle,
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) sessionOwnership.release();
1485
- try {
1486
- if (daemonDispatches.length > 0) {
1487
- await cleanupDaemonDispatches(daemonDispatches);
1488
- }
1489
- } catch (error) {
1081
+ if (!runCompleted) {
1490
1082
  sessionOwnership.release();
1491
- if (!runFailed) {
1492
- runFailed = true;
1493
- runError = error;
1494
- }
1083
+ await agentsRows.release();
1495
1084
  }
1496
-
1497
1085
  if (runFailed) throw runError;
1498
- return { sessionName, results: completedResults, sessionOwnership };
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
- * daemon/native-bridge worker를 read-only로 관찰하는 tmux session을 만든다.
1610
- * 실제 worker process는 daemon에 남고, viewer는 result/partial/stderr 파일만 읽는다.
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
- export function createDaemonObservationSession(
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
- const leadPane = String(opts.leadPane || "");
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.onProgress,
1689
- safeSessionName,
1690
- stage,
1691
- error,
1692
- "observer setup failed",
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 null;
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 에러 발생)