@triflux/remote 10.50.1 → 10.51.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 (120) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +16 -247
  3. package/cto/current.schema.json +4 -20
  4. package/cto/events.mjs +13 -294
  5. package/cto/hygiene.mjs +26 -346
  6. package/cto/lake-root.mjs +114 -16
  7. package/cto/status.mjs +33 -128
  8. package/hub/index.mjs +0 -8
  9. package/hub/store-adapter.mjs +1 -1
  10. package/hub/team/agent-map.json +0 -1
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/cli/commands/attach.mjs +1 -17
  14. package/hub/team/cli/commands/control.mjs +12 -38
  15. package/hub/team/cli/commands/debug.mjs +1 -37
  16. package/hub/team/cli/commands/focus.mjs +5 -31
  17. package/hub/team/cli/commands/interrupt.mjs +6 -25
  18. package/hub/team/cli/commands/kill.mjs +1 -30
  19. package/hub/team/cli/commands/list.mjs +0 -16
  20. package/hub/team/cli/commands/send.mjs +6 -26
  21. package/hub/team/cli/commands/start/index.mjs +41 -133
  22. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  23. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  24. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  25. package/hub/team/cli/commands/status.mjs +2 -57
  26. package/hub/team/cli/commands/stop.mjs +14 -19
  27. package/hub/team/cli/help.mjs +4 -5
  28. package/hub/team/cli/manifest.mjs +5 -1
  29. package/hub/team/cli/render.mjs +0 -12
  30. package/hub/team/cli/services/hub-client.mjs +2 -88
  31. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  32. package/hub/team/handoff.mjs +3 -34
  33. package/hub/team/headless.mjs +14 -755
  34. package/hub/team/orchestrator.mjs +4 -30
  35. package/hub/team/pane.mjs +6 -28
  36. package/hub/team/psmux.mjs +16 -403
  37. package/hub/team/remote-session.mjs +1 -2
  38. package/hub/team/session.mjs +8 -119
  39. package/hub/team/swarm-intent.mjs +0 -198
  40. package/hub/team/terminal-opener.mjs +11 -35
  41. package/hub/team/tui-core.mjs +1 -1
  42. package/hub/team/tui-viewer.mjs +2 -2
  43. package/hub/team/tui.mjs +8 -269
  44. package/hub/team/wt-manager.mjs +2 -2
  45. package/hub/workers/factory.mjs +4 -37
  46. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  47. package/package.json +1 -1
  48. package/scripts/lib/agent-route-policy.mjs +162 -14
  49. package/scripts/lib/claudemd-scanner.mjs +2 -199
  50. package/scripts/lib/cli-claude.mjs +0 -2
  51. package/scripts/lib/context.mjs +3 -19
  52. package/scripts/lib/env-probe.mjs +5 -64
  53. package/scripts/lib/gemini-profiles.mjs +2 -69
  54. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  55. package/scripts/lib/machine-profile.mjs +3 -17
  56. package/scripts/lib/mcp-filter.mjs +109 -53
  57. package/scripts/lib/mcp-health.mjs +2 -2
  58. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  59. package/scripts/lib/psmux-info.mjs +25 -104
  60. package/scripts/lib/skill-template.mjs +0 -271
  61. package/cto/hygiene-actions.mjs +0 -355
  62. package/cto/hygiene-notify.mjs +0 -170
  63. package/hub/team/build-worker-prompt.mjs +0 -92
  64. package/hub/team/check-mcp-hub.mjs +0 -49
  65. package/hub/team/claude-native-bridge.mjs +0 -987
  66. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  67. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  68. package/hub/team/cli/services/native-control.mjs +0 -145
  69. package/hub/team/codex-review.mjs +0 -520
  70. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  71. package/hub/team/conductor.mjs +0 -1529
  72. package/hub/team/consensus-meta.mjs +0 -176
  73. package/hub/team/cto-pull.mjs +0 -31
  74. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  75. package/hub/team/dashboard.mjs +0 -322
  76. package/hub/team/event-log.mjs +0 -84
  77. package/hub/team/execution-mode.mjs +0 -324
  78. package/hub/team/extract-completion-payload.mjs +0 -110
  79. package/hub/team/headless-bridge-session.mjs +0 -17
  80. package/hub/team/health-probe.mjs +0 -435
  81. package/hub/team/interactive-native-launcher.mjs +0 -119
  82. package/hub/team/interactive-native-worker.mjs +0 -80
  83. package/hub/team/interactive-tui-transport.mjs +0 -479
  84. package/hub/team/launcher-template.mjs +0 -144
  85. package/hub/team/mcp-selector.mjs +0 -145
  86. package/hub/team/native-supervisor.mjs +0 -460
  87. package/hub/team/native.mjs +0 -727
  88. package/hub/team/recovery-store.mjs +0 -149
  89. package/hub/team/remote-probe.mjs +0 -292
  90. package/hub/team/remote-watcher.mjs +0 -506
  91. package/hub/team/runtime-strategy.mjs +0 -200
  92. package/hub/team/schemas/worker-completion.json +0 -39
  93. package/hub/team/sentinel-capture.mjs +0 -127
  94. package/hub/team/swarm-cli.mjs +0 -390
  95. package/hub/team/swarm-hypervisor.mjs +0 -2206
  96. package/hub/team/swarm-locks.mjs +0 -335
  97. package/hub/team/swarm-planner.mjs +0 -371
  98. package/hub/team/swarm-preflight.mjs +0 -366
  99. package/hub/team/swarm-reconciler.mjs +0 -242
  100. package/hub/team/tui-remote-adapter.mjs +0 -377
  101. package/hub/team/worker-completion-validator.mjs +0 -79
  102. package/hub/team/worktree-lifecycle.mjs +0 -639
  103. package/hub/team/wt-templates.json +0 -43
  104. package/hub/workers/antigravity-route-worker.mjs +0 -179
  105. package/hub/workers/gemini-worker.mjs +0 -217
  106. package/scripts/lib/agent-json.mjs +0 -27
  107. package/scripts/lib/async.mjs +0 -174
  108. package/scripts/lib/cli-agy.mjs +0 -85
  109. package/scripts/lib/cli-codex.mjs +0 -62
  110. package/scripts/lib/cli-gemini.mjs +0 -37
  111. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  112. package/scripts/lib/env.mjs +0 -14
  113. package/scripts/lib/hub.mjs +0 -48
  114. package/scripts/lib/pid.mjs +0 -63
  115. package/scripts/lib/quota.mjs +0 -47
  116. package/scripts/lib/skill-state.mjs +0 -220
  117. package/scripts/lib/team.mjs +0 -75
  118. package/scripts/lib/timeout.mjs +0 -66
  119. package/scripts/lib/tmp.mjs +0 -26
  120. package/scripts/lib/toml.mjs +0 -132
@@ -1,8 +1,5 @@
1
1
  // hub/team/headless.mjs — 헤드리스 CLI 오케스트레이션
2
2
  // psmux pane에서 CLI를 헤드리스 모드로 실행하고 결과를 수집한다.
3
- // v5.2.0: 기본 headless 엔진 (runHeadless, runHeadlessWithCleanup)
4
- // v6.0.0: Lead-direct 모드 (runHeadlessInteractive, autoAttachTerminal)
5
- // 의존성: psmux.mjs (Node.js 내장 모듈만 사용)
6
3
 
7
4
  import { execSync } from "node:child_process";
8
5
  import { randomUUID } from "node:crypto";
@@ -10,7 +7,6 @@ import {
10
7
  existsSync,
11
8
  mkdirSync,
12
9
  readFileSync,
13
- renameSync,
14
10
  rmSync,
15
11
  statSync,
16
12
  writeFileSync,
@@ -20,10 +16,8 @@ import net from "node:net";
20
16
  import { tmpdir } from "node:os";
21
17
  import { dirname, join, resolve } from "node:path";
22
18
  import { fileURLToPath } from "node:url";
23
- import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
24
- import { requestJson } from "@triflux/core/hub/bridge.mjs";
19
+ import { resolveNestedCodexAgentProfile } from "../../scripts/lib/agent-route-policy.mjs";
25
20
  import { escapePwshSingleQuoted } from "@triflux/core/hub/cli-adapter-base.mjs";
26
- import { getMaxSpawnPerSec } from "@triflux/core/hub/lib/spawn-trace.mjs";
27
21
  import {
28
22
  createActivityLifecycle,
29
23
  isActivityLifecycleEnabled,
@@ -40,7 +34,6 @@ import {
40
34
  killDaemonJob,
41
35
  sendKillBySessionId,
42
36
  } from "./claude-daemon-control.mjs";
43
- import { startClaudeNativeBridge } from "./claude-native-bridge.mjs";
44
37
  import { removeClaudeSessionProjection } from "./claude-session-projection.mjs";
45
38
  import { resolveDashboardLayout } from "./dashboard-layout.mjs";
46
39
  import {
@@ -63,11 +56,6 @@ import {
63
56
  startCapture,
64
57
  waitForCompletion,
65
58
  } from "./psmux.mjs";
66
- import {
67
- buildSynapseTaskSummary,
68
- registerSynapseSession,
69
- unregisterSynapseSession,
70
- } from "./synapse-http.mjs";
71
59
  import { createLogDashboard } from "./tui.mjs";
72
60
  import { createWtManager } from "./wt-manager.mjs";
73
61
 
@@ -78,7 +66,6 @@ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
78
66
  /** CLI별 브랜드 — 이모지 + 공식 색상 (HUD와 통일) */
79
67
  const CLI_BRAND = {
80
68
  codex: { emoji: "\u{26AA}", label: "Codex", ansi: "\x1b[97m" }, // ⚪ bright white (codexWhite)
81
- gemini: { emoji: "\u{1F535}", label: "Gemini", ansi: "\x1b[38;5;39m" }, // 🔵 geminiBlue
82
69
  antigravity: {
83
70
  emoji: "\u{1F535}",
84
71
  label: "Antigravity",
@@ -90,8 +77,6 @@ const CLI_BRAND = {
90
77
  ansi: "\x1b[38;2;232;112;64m",
91
78
  }, // 🟠 claudeOrange
92
79
  };
93
- const _ANSI_RESET = "\x1b[0m";
94
- const _ANSI_DIM = "\x1b[2m";
95
80
 
96
81
  /** 에이전트 역할명 → CLI 타입 매핑 (단일 소스: agent-map.json) */
97
82
  const _require = createRequire(import.meta.url);
@@ -109,11 +94,6 @@ export function resolveCliType(agentOrCli) {
109
94
  return AGENT_TO_CLI[agentOrCli] || agentOrCli;
110
95
  }
111
96
 
112
- function resolveHeadlessCliType(agentOrCli) {
113
- const resolved = resolveCliType(agentOrCli);
114
- return resolved === "gemini" ? "antigravity" : resolved;
115
- }
116
-
117
97
  function shellQuote(value) {
118
98
  return `'${String(value).replace(/'/g, "'\\''")}'`;
119
99
  }
@@ -146,7 +126,7 @@ function resolveRouteAgentForHeadless(resolvedCli, opts = {}) {
146
126
  if (
147
127
  role &&
148
128
  VALID_ROUTE_AGENTS.has(role) &&
149
- !["gemini", "antigravity", "agy"].includes(role)
129
+ !["antigravity", "agy"].includes(role)
150
130
  ) {
151
131
  return role;
152
132
  }
@@ -215,95 +195,6 @@ export function getHeadlessWorkerAgentId(sessionName, index) {
215
195
  return `headless-${sessionName}-${index}`;
216
196
  }
217
197
 
218
- export function getHeadlessLeadAgentId(sessionName) {
219
- return `headless-${sessionName}-lead`;
220
- }
221
-
222
- const HUB_CLI_VALUES = new Set(["codex", "gemini", "claude", "other"]);
223
- const presenceOwnerByAgent = new Map();
224
- function normalizeCliForHub(cli) {
225
- return HUB_CLI_VALUES.has(cli) ? cli : "other";
226
- }
227
-
228
- export async function registerHeadlessWorker(
229
- sessionName,
230
- index,
231
- cli,
232
- requestJsonFn = requestJson,
233
- ) {
234
- const agentId = getHeadlessWorkerAgentId(sessionName, index);
235
- const result = await requestJsonFn("/bridge/register", {
236
- body: {
237
- agent_id: agentId,
238
- cli: normalizeCliForHub(cli),
239
- topics: ["headless.worker"],
240
- capabilities: [cli],
241
- },
242
- }).catch(() => null);
243
- if (result?.ok && result.data?.presence_generation != null) {
244
- presenceOwnerByAgent.set(agentId, {
245
- presence_generation: result.data.presence_generation,
246
- hub_instance_id: result.data.hub_instance_id,
247
- store_fingerprint: result.data.store_fingerprint,
248
- });
249
- }
250
- return result;
251
- }
252
-
253
- export async function publishHeadlessResult(
254
- sessionName,
255
- workerId,
256
- status,
257
- handoff,
258
- requestJsonFn = requestJson,
259
- ) {
260
- await requestJsonFn("/bridge/publish", {
261
- body: {
262
- from: getHeadlessLeadAgentId(sessionName),
263
- to: "topic:headless.results",
264
- type: "event",
265
- payload: { workerId, status, handoff },
266
- },
267
- }).catch(() => {});
268
- }
269
-
270
- export async function deregisterHeadlessWorkers(
271
- sessionName,
272
- workerCount,
273
- requestJsonFn = requestJson,
274
- ) {
275
- await Promise.all(
276
- Array.from({ length: workerCount }, (_, index) =>
277
- requestJsonFn("/bridge/deregister", {
278
- body: {
279
- agent_id: getHeadlessWorkerAgentId(sessionName, index),
280
- ...(presenceOwnerByAgent.get(
281
- getHeadlessWorkerAgentId(sessionName, index),
282
- ) || {}),
283
- },
284
- })
285
- .catch(() => {})
286
- .finally(() =>
287
- presenceOwnerByAgent.delete(
288
- getHeadlessWorkerAgentId(sessionName, index),
289
- ),
290
- ),
291
- ),
292
- );
293
- }
294
-
295
- function registerHeadlessSynapseWorker(workerId, prompt) {
296
- registerSynapseSession({
297
- sessionId: workerId,
298
- host: "local",
299
- taskSummary: buildSynapseTaskSummary(prompt),
300
- });
301
- }
302
-
303
- function unregisterHeadlessSynapseWorker(workerId) {
304
- unregisterSynapseSession(workerId);
305
- }
306
-
307
198
  /** MCP 프로필별 프롬프트 힌트 (tfx-route.sh resolve_mcp_policy의 경량 미러) */
308
199
  const MCP_PROFILE_HINTS = {
309
200
  implement:
@@ -314,19 +205,6 @@ const MCP_PROFILE_HINTS = {
314
205
  docs: "Focus on documentation and explanation tasks.",
315
206
  };
316
207
 
317
- /**
318
- * CLI별 헤드리스 명령 빌더
319
- * @param {'codex'|'antigravity'|'claude'} cli
320
- * @param {string} prompt — 실행할 프롬프트
321
- * @param {string} resultFile — 결과 저장 파일 경로
322
- * @param {object} [opts]
323
- * @param {boolean} [opts.handoff=true]
324
- * @param {string} [opts.mcp] — MCP 프로필 ("implement"|"analyze"|"review"|"docs")
325
- * @param {string} [opts.contextFile] — 컨텍스트 파일 경로 (최대 32KB, UTF-8 안전 절단)
326
- * @returns {string} PowerShell 명령
327
- */
328
- // ── Dashboard attach args for WT ────────────────────────────────
329
-
330
208
  export function buildDashboardAttachArgs(
331
209
  sessionName,
332
210
  layout,
@@ -348,7 +226,7 @@ export function buildDashboardAttachArgs(
348
226
 
349
227
  export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
350
228
  const { handoff = true, mcp, contextFile, model, cwd } = opts;
351
- const resolvedCli = resolveHeadlessCliType(cli);
229
+ const resolvedCli = resolveCliType(cli);
352
230
 
353
231
  // contextFile 처리: 32KB(32768 bytes) 초과 시 UTF-8 안전 절단
354
232
  let contextPrefix = "";
@@ -391,7 +269,6 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
391
269
  "prompt-" + randomUUID().slice(0, 8) + ".txt",
392
270
  ).replace(/\\/g, "/");
393
271
  writeFileSync(promptFile, fullPrompt, "utf8");
394
- void IS_WINDOWS; // referenced for diagnostic guard chain below
395
272
 
396
273
  // Codex와 Antigravity는 tfx-route.sh를 통해서만 headless 실행한다. 이 경로가
397
274
  // disable/fallback/fail-loud 정책의 유일한 판정원이다. 여기서 같은 정책을
@@ -531,83 +408,6 @@ export function readResult(resultFile, paneId) {
531
408
  return capturePsmuxPane(paneId, 30);
532
409
  }
533
410
 
534
- // ─── Stall Detection ───
535
-
536
- /** Stall detection 기본값 (immutable) */
537
- export const STALL_DEFAULTS = Object.freeze({
538
- pollInterval: 5_000,
539
- stallTimeout: 120_000,
540
- interventionTimeout: resolveStallInterventionMs(),
541
- hardCeiling: resolveHardCeilingMs(),
542
- maxRestarts: 2,
543
- maxInterventions: 1,
544
- });
545
-
546
- /** CLI pane stall 감지 에러 (STALL_EXHAUSTED | COMPLETION_TIMEOUT) */
547
- export class StallError extends Error {
548
- constructor(
549
- message,
550
- { code = "STALL_DETECTED", category = "transient", recovery = "" } = {},
551
- ) {
552
- super(message);
553
- this.name = "StallError";
554
- this.code = code;
555
- this.category = category;
556
- this.recovery = recovery;
557
- }
558
- }
559
-
560
- /**
561
- * Stall 모니터 팩토리 — output + resultFile mtime 하이브리드 감지
562
- * @param {string} paneId
563
- * @param {string} resultFile
564
- * @param {{ stallTimeout: number }} config
565
- * @param {{ capturePsmuxPane?: Function, statSync?: Function }} [deps]
566
- * @returns {{ poll: () => { snapshot: string, mtimeChanged: boolean, stalled: boolean, elapsed: number } }}
567
- */
568
- export function createStallMonitor(paneId, resultFile, config, deps = {}) {
569
- const capture = deps.capturePsmuxPane || capturePsmuxPane;
570
- const stat = deps.statSync || statSync;
571
- let lastSnapshot = "";
572
- let lastMtime = 0;
573
- let lastChangeAt = Date.now();
574
-
575
- try {
576
- lastMtime = stat(resultFile).mtimeMs;
577
- } catch {
578
- /* not created yet */
579
- }
580
-
581
- return Object.freeze({
582
- poll() {
583
- const snapshot = capture(paneId, 50);
584
- let currentMtime = 0;
585
- try {
586
- currentMtime = stat(resultFile).mtimeMs;
587
- } catch {
588
- /* ignore */
589
- }
590
-
591
- const outputChanged = snapshot !== lastSnapshot;
592
- const mtimeChanged = currentMtime > 0 && currentMtime !== lastMtime;
593
-
594
- if (outputChanged || mtimeChanged) {
595
- lastChangeAt = Date.now();
596
- lastSnapshot = snapshot;
597
- if (mtimeChanged) lastMtime = currentMtime;
598
- }
599
-
600
- const elapsed = Date.now() - lastChangeAt;
601
- return Object.freeze({
602
- snapshot,
603
- mtimeChanged,
604
- stalled: elapsed >= config.stallTimeout,
605
- elapsed,
606
- });
607
- },
608
- });
609
- }
610
-
611
411
  function createHeadlessIntervention(dispatch, fallback) {
612
412
  return async (context) => {
613
413
  if (typeof fallback === "function")
@@ -706,254 +506,6 @@ export function createActivityPollGuard({
706
506
  });
707
507
  }
708
508
 
709
- /**
710
- * 하이브리드 stall 감지 대기 — output 변화 + resultFile mtime 모니터링.
711
- * 2분 무변화 시 pane kill → re-dispatch (최대 2회 재시작).
712
- *
713
- * @param {string} sessionName
714
- * @param {string} paneId — 현재 pane 타겟 (예: "tfx:0.1")
715
- * @param {string} resultFile — 결과 저장 파일 경로
716
- * @param {object} [opts]
717
- * @param {number} [opts.pollInterval=5000] — 폴링 간격 ms
718
- * @param {number} [opts.stallTimeout=120000] — 무변화 stall 판정 ms
719
- * @param {number} [opts.hardCeiling] — 활동과 무관한 절대 상한 ms
720
- * @param {number} [opts.completionTimeout] — hardCeiling의 하위호환 alias
721
- * @param {number} [opts.maxRestarts=2] — 최대 재시작 횟수
722
- * @param {string} [opts.command] — re-dispatch용 원본 명령
723
- * @param {string} [opts.token] — completion token
724
- * @param {(snapshot: string) => void} [opts.onPoll] — 폴링 콜백
725
- * @returns {Promise<{ matched: boolean, exitCode: number|null, restarts: number, stallDetected: boolean, paneId: string, token?: string, logPath?: string|null }>}
726
- */
727
- export async function waitForCompletionWithStallDetect(
728
- sessionName,
729
- paneId,
730
- resultFile,
731
- opts = {},
732
- ) {
733
- const {
734
- pollInterval = 5000,
735
- stallTimeout = 120000,
736
- maxRestarts = 2,
737
- maxInterventions = 1,
738
- command,
739
- token,
740
- onPoll,
741
- onIntervene,
742
- _deps,
743
- } = opts;
744
- const hardCeiling =
745
- opts.hardCeiling ??
746
- opts.completionTimeout ??
747
- (isActivityLifecycleEnabled() ? resolveHardCeilingMs() : 900_000);
748
-
749
- // 의존성 (테스트 시 _deps로 주입 가능)
750
- const deps = _deps || {};
751
- const _capture = deps.capturePsmuxPane || capturePsmuxPane;
752
- const _exists = deps.existsSync || existsSync;
753
- const _stat = deps.statSync || statSync;
754
- const _readFile = deps.readFileSync || readFileSync;
755
- const _exec = deps.psmuxExec || psmuxExec;
756
- const _dispatch = deps.dispatchCommand || dispatchCommand;
757
- const _startCapture = deps.startCapture || startCapture;
758
-
759
- const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
760
- const buildCompletionRegex = (activeToken) => {
761
- const completionPatterns = [
762
- activeToken
763
- ? `${esc("__TRIFLUX_DONE__:")}${esc(activeToken)}:(\\d+)`
764
- : `${esc("__TRIFLUX_DONE__:")}\\S+:(\\d+)`,
765
- activeToken
766
- ? `${esc("TFX_DONE_")}${esc(activeToken)}:(\\d+)`
767
- : `${esc("TFX_DONE_")}\\S+:(\\d+)`,
768
- ];
769
- return new RegExp(completionPatterns.join("|"), "m");
770
- };
771
-
772
- let restarts = 0;
773
- let interventions = 0;
774
- const runStartedAt = Date.now();
775
- let currentPaneId = paneId;
776
- let stallDetected = false;
777
- let currentToken = token;
778
- let currentLogPath = opts.logPath || null;
779
-
780
- while (true) {
781
- let lastOutput = "";
782
- let lastMtime = 0;
783
- let lastChangeAt = Date.now();
784
-
785
- // 초기 resultFile mtime
786
- try {
787
- if (_exists(resultFile)) lastMtime = _stat(resultFile).mtimeMs;
788
- } catch {
789
- /* 무시 */
790
- }
791
-
792
- while (true) {
793
- await new Promise((r) => setTimeout(r, pollInterval));
794
- const now = Date.now();
795
-
796
- // 절대 상한은 재시작과 활동 여부를 관통하는 유일한 wall-clock kill이다.
797
- if (now - runStartedAt > hardCeiling) {
798
- // Issue #118: timeout kill 전에 capture-pane 출력을 .partial 파일로 persist.
799
- // resultFile(`.txt`)이 아직 생성되지 않았을 수 있으므로 readResult 가 fallback 으로 읽는다.
800
- try {
801
- const partialSnapshot = _capture(currentPaneId, 200);
802
- if (partialSnapshot && partialSnapshot.trim().length > 0) {
803
- const writer = deps.writeFileSync || writeFileSync;
804
- writer(`${resultFile}.partial`, partialSnapshot, "utf8");
805
- }
806
- } catch {
807
- /* best-effort, timeout 은 이미 확정 */
808
- }
809
- return {
810
- matched: false,
811
- exitCode: null,
812
- restarts,
813
- stallDetected,
814
- timedOut: true,
815
- timeoutReason: "hard_ceiling",
816
- paneId: currentPaneId,
817
- token: currentToken,
818
- logPath: currentLogPath,
819
- };
820
- }
821
-
822
- // 1) capture-pane 출력 확인
823
- const currentOutput = _capture(currentPaneId, 50);
824
- if (onPoll) {
825
- try {
826
- onPoll(currentOutput);
827
- } catch {
828
- /* 삼킴 */
829
- }
830
- }
831
-
832
- // 2) completion 토큰 감지
833
- const completionRe = buildCompletionRegex(currentToken);
834
- const completionMatch = completionRe.exec(currentOutput);
835
- if (completionMatch) {
836
- return {
837
- matched: true,
838
- exitCode: Number.parseInt(
839
- completionMatch.slice(1).find(Boolean) || "0",
840
- 10,
841
- ),
842
- restarts,
843
- stallDetected,
844
- timedOut: false,
845
- paneId: currentPaneId,
846
- token: currentToken,
847
- logPath: currentLogPath,
848
- };
849
- }
850
-
851
- // 3) resultFile 존재 + mtime 변화 확인
852
- let currentMtime = 0;
853
- try {
854
- if (_exists(resultFile)) currentMtime = _stat(resultFile).mtimeMs;
855
- } catch {
856
- /* 무시 */
857
- }
858
-
859
- // 4) 변화 감지 → stallTimer 리셋
860
- const outputChanged = currentOutput !== lastOutput;
861
- const mtimeChanged = currentMtime > 0 && currentMtime !== lastMtime;
862
-
863
- if (outputChanged || mtimeChanged) {
864
- lastChangeAt = now;
865
- lastOutput = currentOutput;
866
- if (mtimeChanged) lastMtime = currentMtime;
867
- }
868
-
869
- // resultFile이 갱신되고 내용이 있으면 완료로 간주
870
- if (mtimeChanged && currentMtime > 0 && _exists(resultFile)) {
871
- try {
872
- const content = _readFile(resultFile, "utf8").trim();
873
- if (content.length > 0) {
874
- return {
875
- matched: true,
876
- exitCode: 0,
877
- restarts,
878
- stallDetected,
879
- timedOut: false,
880
- paneId: currentPaneId,
881
- token: currentToken,
882
- logPath: currentLogPath,
883
- };
884
- }
885
- } catch {
886
- /* 무시 */
887
- }
888
- }
889
-
890
- // 5) stall 판정
891
- if (now - lastChangeAt >= stallTimeout) {
892
- stallDetected = true;
893
-
894
- if (restarts >= maxRestarts) {
895
- if (onIntervene && interventions < maxInterventions) {
896
- interventions += 1;
897
- let handled = false;
898
- try {
899
- handled =
900
- (await onIntervene({
901
- channel: "pane",
902
- sessionName,
903
- paneId: currentPaneId,
904
- resultFile,
905
- inactiveMs: now - lastChangeAt,
906
- restarts,
907
- interventions,
908
- })) === true;
909
- } catch {
910
- handled = false;
911
- }
912
- if (handled) {
913
- lastChangeAt = Date.now();
914
- continue;
915
- }
916
- }
917
- const err = new Error("CLI가 반복적으로 멈춤. 수동 확인 필요.");
918
- err.code = "STALL_EXHAUSTED";
919
- err.category = "transient";
920
- err.recovery = "CLI가 반복적으로 멈춤. 수동 확인 필요.";
921
- err.restarts = restarts;
922
- err.interventions = interventions;
923
- throw err;
924
- }
925
-
926
- // kill pane → re-dispatch
927
- try {
928
- _exec(["kill-pane", "-t", currentPaneId]);
929
- } catch {
930
- /* 이미 종료 */
931
- }
932
-
933
- if (command) {
934
- // 새 pane split + 동일 command re-dispatch
935
- const newPaneId = _exec([
936
- "split-window",
937
- "-t",
938
- sessionName,
939
- "-P",
940
- "-F",
941
- "#{session_name}:#{window_index}.#{pane_index}",
942
- ]);
943
- _startCapture(sessionName, newPaneId);
944
- const redispatch = _dispatch(sessionName, newPaneId, command);
945
- currentPaneId = redispatch?.paneId || newPaneId;
946
- if (redispatch?.token) currentToken = redispatch.token;
947
- if (redispatch?.logPath) currentLogPath = redispatch.logPath;
948
- }
949
-
950
- restarts++;
951
- break; // inner loop 재시작 (stallTimer 리셋)
952
- }
953
- }
954
- }
955
- }
956
-
957
509
  function createHeadlessSessionOwnership(killSession) {
958
510
  let owned = false;
959
511
  let ownedSessionName = "";
@@ -1021,7 +573,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
1021
573
  const paneName = `worker-${i + 1}`;
1022
574
  const displayName = resolveHeadlessDisplayName(assignment, paneName);
1023
575
  const workerId = getHeadlessWorkerAgentId(sessionName, i);
1024
- const resolvedCli = resolveHeadlessCliType(assignment.cli);
576
+ const resolvedCli = resolveCliType(assignment.cli);
1025
577
  const brand = CLI_BRAND[resolvedCli] || {
1026
578
  emoji: "\u{25CF}",
1027
579
  label: resolvedCli,
@@ -1047,7 +599,6 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
1047
599
  } catch {
1048
600
  /* 무시 */
1049
601
  }
1050
- await registerHeadlessWorker(sessionName, i, resolvedCli);
1051
602
 
1052
603
  if (safeProgress)
1053
604
  safeProgress({
@@ -1081,7 +632,6 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
1081
632
  // pane 간 pipe-pane EBUSY 방지 — 이벤트 루프 해방하며 순차 대기
1082
633
  if (i > 0) await new Promise((r) => setTimeout(r, 300));
1083
634
  const dispatch = dispatchCommand(sessionName, newPaneId, cmd);
1084
- registerHeadlessSynapseWorker(workerId, assignment.prompt);
1085
635
 
1086
636
  if (safeProgress)
1087
637
  safeProgress({ type: "dispatched", paneName, cli: resolvedCli });
@@ -1150,7 +700,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
1150
700
  const paneName = `worker-${i + 1}`;
1151
701
  const displayName = resolveHeadlessDisplayName(assignment, paneName);
1152
702
  const workerId = getHeadlessWorkerAgentId(sessionName, i);
1153
- const resolvedCli = resolveHeadlessCliType(assignment.cli);
703
+ const resolvedCli = resolveCliType(assignment.cli);
1154
704
  const resultFile = join(
1155
705
  RESULT_DIR,
1156
706
  `${sessionName}-${paneName}.txt`,
@@ -1170,12 +720,10 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
1170
720
  },
1171
721
  );
1172
722
  const scriptDir = join(RESULT_DIR, sessionName);
1173
- await registerHeadlessWorker(sessionName, i, resolvedCli);
1174
723
  const dispatch = dispatchCommand(sessionName, paneName, cmd, {
1175
724
  scriptDir,
1176
725
  scriptName: paneName,
1177
726
  });
1178
- registerHeadlessSynapseWorker(workerId, assignment.prompt);
1179
727
 
1180
728
  // P1 fix: 비-progressive에서는 pane 리네임 금지 — 캡처 로그 경로가 타이틀 기반이므로
1181
729
  // 리네임하면 waitForCompletion이 "codex (role).log"를 찾지만 실제는 "worker-N.log"로 불일치
@@ -1215,7 +763,7 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
1215
763
  const paneName = `worker-${i + 1}`;
1216
764
  const displayName = resolveHeadlessDisplayName(assignment, paneName);
1217
765
  const workerId = getHeadlessWorkerAgentId(sessionName, i);
1218
- const resolvedCli = resolveHeadlessCliType(assignment.cli);
766
+ const resolvedCli = resolveCliType(assignment.cli);
1219
767
  const resultFile = join(
1220
768
  RESULT_DIR,
1221
769
  `${sessionName}-${paneName}.txt`,
@@ -1287,8 +835,6 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
1287
835
  sessionId: dispatched.sessionId,
1288
836
  sessionProjectionPath: dispatched.sessionProjectionPath,
1289
837
  });
1290
- await registerHeadlessWorker(sessionName, i, resolvedCli);
1291
- registerHeadlessSynapseWorker(workerId, assignment.prompt);
1292
838
  if (safeProgress) {
1293
839
  safeProgress({ type: "dispatched", paneName, cli: resolvedCli });
1294
840
  }
@@ -1348,7 +894,6 @@ async function awaitAll(
1348
894
  timeoutSec,
1349
895
  safeProgress,
1350
896
  progressIntervalSec,
1351
- stallOpts,
1352
897
  lifecycle,
1353
898
  ) {
1354
899
  // 병렬 대기 (Promise.all — 모든 pane 동시 폴링, 총 시간 = max(개별 시간))
@@ -1375,73 +920,7 @@ async function awaitAll(
1375
920
  }
1376
921
 
1377
922
  let completion;
1378
- if (stallOpts?.enabled) {
1379
- // 하이브리드 stall detection 모드
1380
- try {
1381
- const stallPollCb =
1382
- safeProgress && progressIntervalSec > 0
1383
- ? (snapshot) => {
1384
- try {
1385
- safeProgress({
1386
- type: "progress",
1387
- paneName: d.paneName,
1388
- displayName: d.displayName,
1389
- cli: d.cli,
1390
- snapshot: snapshot.split("\n").slice(-15).join("\n"),
1391
- });
1392
- } catch {
1393
- /* 삼킴 */
1394
- }
1395
- }
1396
- : undefined;
1397
-
1398
- const stallResult = await waitForCompletionWithStallDetect(
1399
- sessionName,
1400
- d.paneId || d.paneName,
1401
- d.resultFile,
1402
- {
1403
- pollInterval: stallOpts.pollInterval,
1404
- stallTimeout: stallOpts.stallTimeout,
1405
- hardCeiling: lifecycle?.enabled
1406
- ? (stallOpts.hardCeiling ??
1407
- stallOpts.completionTimeout ??
1408
- lifecycle.hardCeilingMs)
1409
- : (stallOpts.completionTimeout ?? timeoutSec * 1000),
1410
- maxRestarts: stallOpts.maxRestarts,
1411
- maxInterventions: stallOpts.maxInterventions,
1412
- command: d.command,
1413
- token: d.token,
1414
- onPoll: stallPollCb,
1415
- onIntervene: lifecycle?.enabled
1416
- ? createHeadlessIntervention(
1417
- d,
1418
- stallOpts.onIntervene ?? lifecycle.onIntervene,
1419
- )
1420
- : undefined,
1421
- },
1422
- );
1423
- if (stallResult.paneId) d.paneId = stallResult.paneId;
1424
- if (stallResult.token) d.token = stallResult.token;
1425
- if (stallResult.logPath) d.logPath = stallResult.logPath;
1426
- completion = {
1427
- matched: stallResult.matched,
1428
- exitCode: stallResult.exitCode,
1429
- stallDetected: stallResult.stallDetected,
1430
- restarts: stallResult.restarts,
1431
- };
1432
- } catch (stallErr) {
1433
- if (stallErr.code === "STALL_EXHAUSTED") {
1434
- completion = {
1435
- matched: false,
1436
- exitCode: null,
1437
- stallExhausted: true,
1438
- restarts: stallErr.restarts,
1439
- };
1440
- } else {
1441
- throw stallErr;
1442
- }
1443
- }
1444
- } else if (lifecycle?.enabled) {
923
+ if (lifecycle?.enabled) {
1445
924
  if (d.logPath) pollOpts.logPath = d.logPath;
1446
925
  const activity = createActivityLifecycle({
1447
926
  interventionMs: lifecycle.interventionMs,
@@ -1501,7 +980,6 @@ async function awaitAll(
1501
980
  }
1502
981
 
1503
982
  const output = readResult(d.resultFile, d.paneId);
1504
- unregisterHeadlessSynapseWorker(d.workerId);
1505
983
 
1506
984
  if (safeProgress) {
1507
985
  safeProgress({
@@ -1512,8 +990,6 @@ async function awaitAll(
1512
990
  matched: completion.matched,
1513
991
  exitCode: completion.exitCode,
1514
992
  sessionDead: completion.sessionDead || false,
1515
- stallDetected: completion.stallDetected || false,
1516
- stallExhausted: completion.stallExhausted || false,
1517
993
  });
1518
994
  }
1519
995
 
@@ -1667,7 +1143,6 @@ async function awaitAllDaemon(
1667
1143
  lifecycle,
1668
1144
  );
1669
1145
  const output = readResult(d.resultFile, d.paneId);
1670
- unregisterHeadlessSynapseWorker(d.workerId);
1671
1146
 
1672
1147
  if (safeProgress) {
1673
1148
  safeProgress({
@@ -1678,8 +1153,6 @@ async function awaitAllDaemon(
1678
1153
  matched: completion.matched,
1679
1154
  exitCode: completion.exitCode,
1680
1155
  sessionDead: false,
1681
- stallDetected: false,
1682
- stallExhausted: false,
1683
1156
  });
1684
1157
  }
1685
1158
 
@@ -1693,7 +1166,7 @@ async function awaitAllDaemon(
1693
1166
  * @param {Array<{d, completion, output}>} results
1694
1167
  * @returns {Array}
1695
1168
  */
1696
- async function collectResults(sessionName, results) {
1169
+ async function collectResults(results) {
1697
1170
  // handoff 파이프라인: parse → validate → format (각 워커 결과에 적용)
1698
1171
  return await Promise.all(
1699
1172
  results.map(async ({ d, completion, output }) => {
@@ -1715,18 +1188,6 @@ async function collectResults(sessionName, results) {
1715
1188
  };
1716
1189
  handoffResult.formatted = formatHandoffForLead(handoffResult.handoff);
1717
1190
  }
1718
- const status =
1719
- handoffResult.handoff?.status ||
1720
- (completion.matched && completion.exitCode === 0
1721
- ? "completed"
1722
- : "failed");
1723
- await publishHeadlessResult(
1724
- sessionName,
1725
- d.workerId,
1726
- status,
1727
- handoffResult.handoff,
1728
- );
1729
-
1730
1191
  return {
1731
1192
  cli: d.cli,
1732
1193
  paneName: d.paneName,
@@ -1791,9 +1252,8 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
1791
1252
  autoAttach = false,
1792
1253
  dashboard = false,
1793
1254
  dashboardLayout = "single",
1794
- stallDetect,
1795
1255
  nativeBridge = false,
1796
- nativeBridgeMode = "roster",
1256
+ nativeBridgeMode = "agents",
1797
1257
  onIntervene,
1798
1258
  leadPane = null,
1799
1259
  leadTmux = null,
@@ -1817,67 +1277,11 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
1817
1277
  return { sessionName, results: [], sessionOwnership };
1818
1278
  }
1819
1279
 
1820
- let nativeBridgeHandle = null;
1821
1280
  let daemonDispatches = [];
1822
1281
  let runCompleted = false;
1823
1282
  let completedResults = [];
1824
1283
  let runFailed = false;
1825
1284
  let runError;
1826
- if (nativeBridge && nativeBridgeMode === "claude-wrapper") {
1827
- throw new Error(
1828
- "[headless] --native-bridge-mode claude-wrapper is reserved and not implemented yet",
1829
- );
1830
- }
1831
- if (
1832
- nativeBridge &&
1833
- (nativeBridgeMode === "roster" || nativeBridgeMode === "interactive-attach")
1834
- ) {
1835
- nativeBridgeHandle = await startClaudeNativeBridge({
1836
- sessionName,
1837
- assignments: normalizedAssignments,
1838
- cwd: process.cwd(),
1839
- workerType:
1840
- nativeBridgeMode === "interactive-attach" ? "interactive" : "headless",
1841
- onKill() {
1842
- sessionOwnership.release();
1843
- },
1844
- });
1845
- process.stderr.write(
1846
- `[headless] Claude native bridge roster written: ${nativeBridgeHandle.rosterPath}\n`,
1847
- );
1848
- }
1849
-
1850
- // Hub version skew pre-flight (fail-open, best-effort)
1851
- requestJson("/status", { method: "GET", timeoutMs: 500 })
1852
- .then((status) => {
1853
- const hubRate = status?.spawn_trace?.max_per_sec;
1854
- const localRate = getMaxSpawnPerSec();
1855
- if (typeof hubRate === "number" && hubRate !== localRate) {
1856
- console.warn(
1857
- `[headless] Hub version skew detected: hub spawn rate=${hubRate}/s, local=${localRate}/s. Restart hub to sync.`,
1858
- );
1859
- }
1860
- })
1861
- .catch(() => {});
1862
-
1863
- // Synapse: 세션 registration (fire-and-forget, hub 미응답 시 무시)
1864
- const synapseIds = normalizedAssignments.map(
1865
- (_, i) => `${sessionName}-worker-${i + 1}`,
1866
- );
1867
- for (let i = 0; i < normalizedAssignments.length; i++) {
1868
- const a = normalizedAssignments[i];
1869
- requestJson("/synapse/register", {
1870
- method: "POST",
1871
- body: {
1872
- sessionId: synapseIds[i],
1873
- host: "local",
1874
- taskSummary: String(a.prompt || "").slice(0, 100),
1875
- isRemote: false,
1876
- },
1877
- timeoutMs: 1000,
1878
- }).catch(() => {});
1879
- }
1880
-
1881
1285
  // in-process TUI: dashboard=true이고 stdout이 TTY일 때 직접 구동
1882
1286
  let tui = null;
1883
1287
  const resolvedLayout = resolveDashboardLayout(
@@ -1897,7 +1301,7 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
1897
1301
  const a = normalizedAssignments[i];
1898
1302
  const paneName = `worker-${i + 1}`;
1899
1303
  tui.updateWorker(paneName, {
1900
- cli: resolveHeadlessCliType(a.cli || "codex"),
1304
+ cli: resolveCliType(a.cli || "codex"),
1901
1305
  displayName: resolveHeadlessDisplayName(a, paneName),
1902
1306
  role: a.role || "",
1903
1307
  status: "pending",
@@ -1905,11 +1309,10 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
1905
1309
  });
1906
1310
  }
1907
1311
  } else if (dashboard) {
1908
- // Issue #116-F: --dashboard 요청이 non-TTY 환경에서 silent skip 되던 혼란 해소.
1909
- // stderr 로만 안내 (stdout 은 readHeadlessResult 파싱 대상이라 건드리지 않는다).
1312
+ // stdout은 결과 파싱 대상이므로 안내는 stderr로 보낸다.
1910
1313
  const logDir = join(tmpdir(), "tfx-headless");
1911
1314
  process.stderr.write(
1912
- `\n⚠ --dashboard requested but stdout is not a TTY; dashboard is skipped.\n` +
1315
+ `\n⚠ stdout is not a TTY; dashboard is skipped.\n` +
1913
1316
  ` Session is running in background. Worker logs:\n` +
1914
1317
  ` ${logDir}${IS_WINDOWS ? "\\" : "/"}${sessionName}-worker-N.txt\n` +
1915
1318
  ` ${logDir}${IS_WINDOWS ? "\\" : "/"}${sessionName}-worker-N.txt.err\n\n`,
@@ -1951,62 +1354,9 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
1951
1354
  }
1952
1355
  }
1953
1356
 
1954
- // Synapse heartbeat: progress 이벤트마다 해당 워커의 세션 갱신
1955
- const feedSynapse = (event) => {
1956
- if (!event?.paneName) return;
1957
- const match = event.paneName.match(/worker-(\d+)/);
1958
- if (!match) return;
1959
- const idx = parseInt(match[1], 10) - 1;
1960
- const sid = synapseIds[idx];
1961
- if (!sid) return;
1962
- requestJson("/synapse/heartbeat", {
1963
- method: "POST",
1964
- body: {
1965
- sessionId: sid,
1966
- partial: { taskSummary: (event.snapshot || "").slice(0, 100) },
1967
- },
1968
- timeoutMs: 500,
1969
- }).catch(() => {});
1970
- };
1971
-
1972
- const HUB_ACTIVITY_MIN_INTERVAL_MS = 30_000;
1973
- const lastHubBeatByWorker = new Map();
1974
- const feedHubActivity = (event) => {
1975
- if (event?.type !== "progress" || !event.paneName) return;
1976
- const match = event.paneName.match(/worker-(\d+)/);
1977
- if (!match) return;
1978
- const idx = parseInt(match[1], 10) - 1;
1979
- const agentId = getHeadlessWorkerAgentId(sessionName, idx);
1980
- const nowMs = Date.now();
1981
- if (
1982
- (lastHubBeatByWorker.get(agentId) || 0) + HUB_ACTIVITY_MIN_INTERVAL_MS >
1983
- nowMs
1984
- )
1985
- return;
1986
- lastHubBeatByWorker.set(agentId, nowMs);
1987
- requestJson("/bridge/heartbeat", {
1988
- method: "POST",
1989
- body: {
1990
- agent_id: agentId,
1991
- ...(presenceOwnerByAgent.get(agentId) || {}),
1992
- },
1993
- timeoutMs: 500,
1994
- }).catch(() => {});
1995
- const assignJobId = normalizedAssignments[idx]?.assignJobId;
1996
- if (assignJobId)
1997
- requestJson("/bridge/assign/result", {
1998
- method: "POST",
1999
- body: { job_id: assignJobId, worker_agent: agentId, status: "running" },
2000
- timeoutMs: 500,
2001
- }).catch(() => {});
2002
- };
2003
-
2004
1357
  // onProgress 예외를 삼켜 실행 흐름 보호 (onPoll과 동일 패턴)
2005
1358
  const combinedProgress = (event) => {
2006
- nativeBridgeHandle?.handleProgress(event);
2007
1359
  feedTui(event);
2008
- feedSynapse(event);
2009
- feedHubActivity(event);
2010
1360
  if (onProgress) {
2011
1361
  try {
2012
1362
  onProgress(event);
@@ -2097,11 +1447,9 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
2097
1447
  timeoutSec,
2098
1448
  safeProgress,
2099
1449
  progressIntervalSec,
2100
- stallDetect,
2101
1450
  lifecycle,
2102
1451
  );
2103
- const collected = await collectResults(sessionName, results);
2104
- for (const result of collected) nativeBridgeHandle?.completeWorker(result);
1452
+ const collected = await collectResults(results);
2105
1453
 
2106
1454
  // 완료 시 TUI에 최종 상태 반영 후 닫기
2107
1455
  if (tui) {
@@ -2135,15 +1483,6 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
2135
1483
 
2136
1484
  if (!runCompleted) sessionOwnership.release();
2137
1485
  try {
2138
- // Synapse: 세션 unregister (fire-and-forget)
2139
- for (const sid of synapseIds) {
2140
- requestJson("/synapse/unregister", {
2141
- method: "POST",
2142
- body: { sessionId: sid },
2143
- timeoutMs: 1000,
2144
- }).catch(() => {});
2145
- }
2146
- if (nativeBridgeHandle) await nativeBridgeHandle.close();
2147
1486
  if (daemonDispatches.length > 0) {
2148
1487
  await cleanupDaemonDispatches(daemonDispatches);
2149
1488
  }
@@ -2182,16 +1521,7 @@ export async function runHeadlessWithCleanup(assignments, opts = {}) {
2182
1521
  _sessionOwnership: sessionOwnership,
2183
1522
  });
2184
1523
  } finally {
2185
- try {
2186
- for (let index = 0; index < assignments.length; index++) {
2187
- unregisterHeadlessSynapseWorker(
2188
- getHeadlessWorkerAgentId(sessionName, index),
2189
- );
2190
- }
2191
- await deregisterHeadlessWorkers(sessionName, assignments.length);
2192
- } finally {
2193
- sessionOwnership.release();
2194
- }
1524
+ sessionOwnership.release();
2195
1525
  // WT split pane은 psmux 종료 시 셸이 끝나면서 자동으로 닫힘
2196
1526
  // 수동 close-pane 불필요 (레이스 컨디션으로 WT 에러 발생)
2197
1527
  }
@@ -2233,71 +1563,6 @@ export function applyTrifluxTheme(sessionName) {
2233
1563
  }
2234
1564
  }
2235
1565
 
2236
- /**
2237
- * Windows Terminal에 triflux 프로필을 자동 생성/갱신한다.
2238
- * 반투명 + 비포커스 시 더 투명 + Catppuccin 테마.
2239
- * @returns {boolean} 성공 여부
2240
- */
2241
-
2242
- /**
2243
- * WT 기본 프로필의 폰트 크기를 읽는다.
2244
- * @returns {number} 기본 폰트 크기 (못 읽으면 12)
2245
- */
2246
- function _getWtDefaultFontSize() {
2247
- const settingsPaths = [
2248
- join(
2249
- process.env.LOCALAPPDATA || "",
2250
- "Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json",
2251
- ),
2252
- join(
2253
- process.env.LOCALAPPDATA || "",
2254
- "Microsoft/Windows Terminal/settings.json",
2255
- ),
2256
- ];
2257
- for (const p of settingsPaths) {
2258
- if (!existsSync(p)) continue;
2259
- try {
2260
- const settings = JSON.parse(
2261
- readFileSync(p, "utf8").replace(/^\s*\/\/.*$/gm, ""),
2262
- );
2263
- // 기본 프로필 or 첫 프로필의 폰트
2264
- const defaultGuid = settings.defaultProfile;
2265
- const profiles = settings.profiles?.list || [];
2266
- const defaultProfile =
2267
- profiles.find((pr) => pr.guid === defaultGuid) || profiles[0];
2268
- return (
2269
- defaultProfile?.font?.size ||
2270
- settings.profiles?.defaults?.font?.size ||
2271
- 12
2272
- );
2273
- } catch {
2274
- /* 다음 */
2275
- }
2276
- }
2277
- return 12;
2278
- }
2279
-
2280
- /**
2281
- * 파일을 원자적으로 쓴다 — 임시 파일에 먼저 기록 후 rename으로 교체.
2282
- * 프로세스가 쓰기 도중 충돌해도 원본 파일이 손상되지 않는다.
2283
- * @param {string} filePath — 대상 파일 경로
2284
- * @param {string} data — 쓸 내용
2285
- */
2286
- function _atomicWriteSync(filePath, data) {
2287
- const tmpPath = `${filePath}.${process.pid}.tmp`;
2288
- try {
2289
- writeFileSync(tmpPath, data, "utf8");
2290
- renameSync(tmpPath, filePath);
2291
- } catch (err) {
2292
- try {
2293
- writeFileSync(tmpPath.replace(/\.tmp$/, ".tmp.del"), "");
2294
- } catch {
2295
- /* 무시 */
2296
- }
2297
- throw err;
2298
- }
2299
- }
2300
-
2301
1566
  function sanitizeSessionName(value) {
2302
1567
  return String(value || "").replace(/[^a-zA-Z0-9_-]/g, "") || "tfx-session";
2303
1568
  }
@@ -2876,12 +2141,6 @@ export async function runHeadlessInteractive(
2876
2141
  kill() {
2877
2142
  if (this._killed) return;
2878
2143
  this._killed = true;
2879
- for (let index = 0; index < assignments.length; index++) {
2880
- unregisterHeadlessSynapseWorker(
2881
- getHeadlessWorkerAgentId(sessionName, index),
2882
- );
2883
- }
2884
- void deregisterHeadlessWorkers(sessionName, assignments.length);
2885
2144
  sessionOwnership.release();
2886
2145
  // attach pane/tab은 psmux 종료 → attach client 종료 → 자동 닫힘
2887
2146
  // 수동 close-pane 불필요 (레이스 컨디션으로 WT 0x80070002 에러 발생)