@triflux/remote 10.50.2 → 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 (97) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +0 -73
  3. package/cto/current.schema.json +0 -4
  4. package/cto/status.mjs +2 -50
  5. package/hub/index.mjs +0 -8
  6. package/hub/team/agent-map.json +0 -1
  7. package/hub/team/ansi.mjs +1 -35
  8. package/hub/team/backend.mjs +9 -145
  9. package/hub/team/cli/commands/attach.mjs +1 -17
  10. package/hub/team/cli/commands/control.mjs +12 -38
  11. package/hub/team/cli/commands/debug.mjs +1 -37
  12. package/hub/team/cli/commands/focus.mjs +5 -31
  13. package/hub/team/cli/commands/interrupt.mjs +6 -25
  14. package/hub/team/cli/commands/kill.mjs +1 -30
  15. package/hub/team/cli/commands/list.mjs +0 -16
  16. package/hub/team/cli/commands/send.mjs +6 -26
  17. package/hub/team/cli/commands/start/index.mjs +41 -133
  18. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  19. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  20. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  21. package/hub/team/cli/commands/status.mjs +2 -57
  22. package/hub/team/cli/commands/stop.mjs +14 -19
  23. package/hub/team/cli/help.mjs +4 -5
  24. package/hub/team/cli/manifest.mjs +5 -1
  25. package/hub/team/cli/render.mjs +0 -12
  26. package/hub/team/cli/services/hub-client.mjs +2 -88
  27. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  28. package/hub/team/handoff.mjs +3 -34
  29. package/hub/team/headless.mjs +13 -754
  30. package/hub/team/orchestrator.mjs +4 -30
  31. package/hub/team/pane.mjs +6 -28
  32. package/hub/team/psmux.mjs +0 -1
  33. package/hub/team/remote-session.mjs +1 -2
  34. package/hub/team/session.mjs +7 -91
  35. package/hub/team/swarm-intent.mjs +0 -198
  36. package/hub/team/tui-core.mjs +1 -1
  37. package/hub/team/tui-viewer.mjs +2 -2
  38. package/hub/team/tui.mjs +8 -269
  39. package/hub/team/wt-manager.mjs +2 -2
  40. package/hub/workers/factory.mjs +4 -37
  41. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  42. package/package.json +1 -1
  43. package/scripts/lib/agent-route-policy.mjs +162 -14
  44. package/scripts/lib/claudemd-scanner.mjs +2 -199
  45. package/scripts/lib/cli-claude.mjs +0 -2
  46. package/scripts/lib/context.mjs +3 -19
  47. package/scripts/lib/env-probe.mjs +5 -64
  48. package/scripts/lib/gemini-profiles.mjs +2 -69
  49. package/scripts/lib/machine-profile.mjs +3 -17
  50. package/scripts/lib/mcp-filter.mjs +109 -53
  51. package/scripts/lib/mcp-health.mjs +2 -2
  52. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  53. package/scripts/lib/psmux-info.mjs +11 -59
  54. package/scripts/lib/skill-template.mjs +0 -245
  55. package/hub/team/build-worker-prompt.mjs +0 -92
  56. package/hub/team/check-mcp-hub.mjs +0 -49
  57. package/hub/team/claude-native-bridge.mjs +0 -987
  58. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  59. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  60. package/hub/team/cli/services/native-control.mjs +0 -145
  61. package/hub/team/codex-review.mjs +0 -520
  62. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  63. package/hub/team/conductor.mjs +0 -1529
  64. package/hub/team/consensus-meta.mjs +0 -176
  65. package/hub/team/cto-pull.mjs +0 -31
  66. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  67. package/hub/team/dashboard.mjs +0 -322
  68. package/hub/team/event-log.mjs +0 -84
  69. package/hub/team/execution-mode.mjs +0 -324
  70. package/hub/team/extract-completion-payload.mjs +0 -110
  71. package/hub/team/headless-bridge-session.mjs +0 -17
  72. package/hub/team/health-probe.mjs +0 -435
  73. package/hub/team/interactive-native-launcher.mjs +0 -119
  74. package/hub/team/interactive-native-worker.mjs +0 -80
  75. package/hub/team/interactive-tui-transport.mjs +0 -479
  76. package/hub/team/launcher-template.mjs +0 -144
  77. package/hub/team/mcp-selector.mjs +0 -145
  78. package/hub/team/native-supervisor.mjs +0 -460
  79. package/hub/team/native.mjs +0 -727
  80. package/hub/team/recovery-store.mjs +0 -149
  81. package/hub/team/remote-probe.mjs +0 -292
  82. package/hub/team/remote-watcher.mjs +0 -506
  83. package/hub/team/runtime-strategy.mjs +0 -200
  84. package/hub/team/schemas/worker-completion.json +0 -39
  85. package/hub/team/sentinel-capture.mjs +0 -127
  86. package/hub/team/swarm-cli.mjs +0 -390
  87. package/hub/team/swarm-hypervisor.mjs +0 -2206
  88. package/hub/team/swarm-locks.mjs +0 -335
  89. package/hub/team/swarm-planner.mjs +0 -371
  90. package/hub/team/swarm-preflight.mjs +0 -366
  91. package/hub/team/swarm-reconciler.mjs +0 -242
  92. package/hub/team/tui-remote-adapter.mjs +0 -377
  93. package/hub/team/worker-completion-validator.mjs +0 -79
  94. package/hub/team/worktree-lifecycle.mjs +0 -639
  95. package/hub/team/wt-templates.json +0 -43
  96. package/hub/workers/antigravity-route-worker.mjs +0 -179
  97. package/scripts/lib/dynamic-route-cli.mjs +0 -107
@@ -6,7 +6,7 @@ import {
6
6
  } from "../../../headless.mjs";
7
7
  import { AMBER, DIM, GREEN, RESET } from "../../../shared.mjs";
8
8
  import { ok } from "../../render.mjs";
9
- import { clearTeamState } from "../../services/state-store.mjs";
9
+ import { clearTeamState, saveTeamState } from "../../services/state-store.mjs";
10
10
  import { buildTasks } from "../../services/task-model.mjs";
11
11
 
12
12
  export function buildHeadlessAssignments({
@@ -66,6 +66,7 @@ export async function startHeadlessTeam({
66
66
  cwd,
67
67
  nativeBridge,
68
68
  nativeBridgeMode,
69
+ _deps = {},
69
70
  }) {
70
71
  const assignments = buildHeadlessAssignments({
71
72
  agents,
@@ -76,30 +77,59 @@ export async function startHeadlessTeam({
76
77
  cwd,
77
78
  });
78
79
 
79
- const _startedAt = Date.now();
80
+ const state = {
81
+ sessionId,
82
+ sessionName: sessionId,
83
+ task,
84
+ lead,
85
+ agents: assignments.map((assignment) => assignment.cli),
86
+ layout,
87
+ teammateMode: "headless",
88
+ ownerPid: process.pid,
89
+ startedAt: Date.now(),
90
+ members: assignments.map((assignment, index) => ({
91
+ role: "worker",
92
+ name: `worker-${index + 1}`,
93
+ cli: assignment.cli,
94
+ pane: "",
95
+ subtask: assignment.prompt,
96
+ })),
97
+ };
98
+ state.tasks = buildTasks(
99
+ assignments.map((assignment) => assignment.prompt),
100
+ state.members,
101
+ );
102
+ saveTeamState(state, sessionId);
80
103
  ok(`headless ${assignments.length}워커 시작`);
81
104
  if (nativeBridge) {
82
105
  process.stderr.write(
83
- `[headless] Claude native bridge mode: ${nativeBridgeMode || "roster"}\n`,
106
+ `[headless] Claude native bridge mode: ${nativeBridgeMode || "agents"}\n`,
84
107
  );
85
108
  }
86
109
 
87
- const handle = await runHeadlessInteractive(sessionId, assignments, {
88
- timeoutSec: timeoutSec || 900,
89
- layout,
90
- autoAttach: !!autoAttach,
91
- leadPane: process.env.TMUX_PANE,
92
- leadTmux: process.env.TMUX,
93
- dashboard: !!dashboard,
94
- dashboardLayout,
95
- dashboardSize: dashboardSize ?? 0.5,
96
- dashboardAnchor,
97
- progressive: progressive !== false,
98
- nativeBridge,
99
- nativeBridgeMode,
100
- progressIntervalSec: verbose ? 10 : 0,
101
- onProgress: verbose
102
- ? function onProgress(event) {
110
+ let handle;
111
+ try {
112
+ const run = _deps.runHeadlessInteractive || runHeadlessInteractive;
113
+ handle = await run(sessionId, assignments, {
114
+ timeoutSec: timeoutSec || 900,
115
+ layout,
116
+ autoAttach: !!autoAttach,
117
+ leadPane: process.env.TMUX_PANE,
118
+ leadTmux: process.env.TMUX,
119
+ dashboard: !!dashboard,
120
+ dashboardLayout,
121
+ dashboardSize: dashboardSize ?? 0.5,
122
+ dashboardAnchor,
123
+ progressive: progressive !== false,
124
+ nativeBridge,
125
+ nativeBridgeMode,
126
+ progressIntervalSec: verbose ? 10 : 0,
127
+ onProgress(event) {
128
+ if (event.type === "session_created") {
129
+ state.sessionName = event.sessionName;
130
+ saveTeamState(state, sessionId);
131
+ }
132
+ if (verbose) {
103
133
  if (event.type === "session_created") {
104
134
  console.log(` ${DIM}세션: ${event.sessionName}${RESET}`);
105
135
  } else if (event.type === "worker_added") {
@@ -128,104 +158,107 @@ export async function startHeadlessTeam({
128
158
  );
129
159
  }
130
160
  }
131
- : undefined,
132
- });
133
-
134
- // 최소 결과 요약
135
- const results = handle.results;
136
- const succeeded = results.filter((r) => r.matched && r.exitCode === 0);
137
- const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
161
+ },
162
+ });
138
163
 
139
- ok(
140
- `헤드리스 완료: ${succeeded.length}성공 / ${failed.length}실패 / ${results.length}전체`,
141
- );
142
- // 머신 파싱용 완료 마커 — background task에서 결과 회수 시점 판단에 사용
143
- console.log(
144
- `=== HEADLESS_COMPLETE succeeded=${succeeded.length} failed=${failed.length} total=${results.length} ===`,
145
- );
164
+ // 최소 결과 요약
165
+ const results = handle.results;
166
+ const succeeded = results.filter((r) => r.matched && r.exitCode === 0);
167
+ const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
146
168
 
147
- if (failed.length > 0) {
148
- for (const r of failed)
149
- console.log(
150
- ` ${AMBER}✗${RESET} ${r.paneName} (${r.cli}) exit=${r.exitCode}`,
151
- );
152
- }
169
+ ok(
170
+ `헤드리스 완료: ${succeeded.length}성공 / ${failed.length}실패 / ${results.length}전체`,
171
+ );
172
+ // background task가 이 완료 마커로 결과 회수 시점을 판단한다.
173
+ console.log(
174
+ `=== HEADLESS_COMPLETE succeeded=${succeeded.length} failed=${failed.length} total=${results.length} ===`,
175
+ );
153
176
 
154
- // handoff 요약 (Lead 토큰 절약 포맷)
155
- for (const r of results) {
156
- const icon =
157
- r.matched && r.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
158
- if (r.handoffFormatted) {
159
- const tag = r.handoffFallback ? `${DIM}(fallback)${RESET}` : "";
160
- console.log(` ${icon} ${r.paneName} ${tag}`);
161
- for (const line of r.handoffFormatted.split("\n")) {
162
- console.log(` ${DIM}${line}${RESET}`);
163
- }
164
- } else {
165
- if (r.resultFile) console.log(` ${icon} ${r.paneName}: ${r.resultFile}`);
177
+ if (failed.length > 0) {
178
+ for (const r of failed)
179
+ console.log(
180
+ ` ${AMBER}✗${RESET} ${r.paneName} (${r.cli}) exit=${r.exitCode}`,
181
+ );
166
182
  }
167
- }
168
183
 
169
- // --verbose: 기존 장황한 출력 (200자 preview)
170
- if (verbose) {
184
+ // handoff 요약 (Lead 토큰 절약 포맷)
171
185
  for (const r of results) {
172
- if (r.output) {
173
- const preview =
174
- r.output.length > 200 ? `${r.output.slice(0, 200)}…` : r.output;
175
- console.log(
176
- `\n ${DIM}── ${r.paneName} (${r.cli}${r.role ? `, ${r.role}` : ""}) ──${RESET}`,
177
- );
178
- console.log(` ${preview}`);
186
+ const icon =
187
+ r.matched && r.exitCode === 0
188
+ ? `${GREEN}✓${RESET}`
189
+ : `${AMBER}✗${RESET}`;
190
+ if (r.handoffFormatted) {
191
+ const tag = r.handoffFallback ? `${DIM}(fallback)${RESET}` : "";
192
+ console.log(` ${icon} ${r.paneName} ${tag}`);
193
+ for (const line of r.handoffFormatted.split("\n")) {
194
+ console.log(` ${DIM}${line}${RESET}`);
195
+ }
196
+ } else {
197
+ if (r.resultFile)
198
+ console.log(` ${icon} ${r.paneName}: ${r.resultFile}`);
179
199
  }
180
200
  }
181
- }
182
201
 
183
- // dashboard 모드: tui-viewer가 최종 상태를 렌더링할 시간 확보
184
- // WT pane spawn (~1s) + node 기동 (~500ms) + 첫 폴링 (~500ms) + 렌더 여유
185
- if (dashboard) await new Promise((r) => setTimeout(r, 5000));
202
+ // --verbose: 기존 장황한 출력 (200자 preview)
203
+ if (verbose) {
204
+ for (const r of results) {
205
+ if (r.output) {
206
+ const preview =
207
+ r.output.length > 200 ? `${r.output.slice(0, 200)}…` : r.output;
208
+ console.log(
209
+ `\n ${DIM}── ${r.paneName} (${r.cli}${r.role ? `, ${r.role}` : ""}) ──${RESET}`,
210
+ );
211
+ console.log(` ${preview}`);
212
+ }
213
+ }
214
+ }
186
215
 
187
- // 세션 정리
188
- handle.kill();
216
+ // dashboard 모드: tui-viewer가 최종 상태를 렌더링할 시간 확보
217
+ // WT pane spawn (~1s) + node 기동 (~500ms) + 첫 폴링 (~500ms) + 렌더 여유
218
+ if (dashboard) await new Promise((r) => setTimeout(r, 5000));
189
219
 
190
- const members = [
191
- {
192
- role: "lead",
193
- name: "lead",
194
- cli: lead,
195
- pane: `${handle.sessionName}:0.0`,
196
- },
197
- ...results.map((r, i) => ({
198
- role: "worker",
199
- name: r.paneName,
200
- cli: r.cli,
201
- pane: r.paneId || "",
202
- subtask: assignments[i]?.prompt,
203
- })),
204
- ];
220
+ const members = [
221
+ {
222
+ role: "lead",
223
+ name: "lead",
224
+ cli: lead,
225
+ pane: `${handle.sessionName}:0.0`,
226
+ },
227
+ ...results.map((r, i) => ({
228
+ role: "worker",
229
+ name: r.paneName,
230
+ cli: r.cli,
231
+ pane: r.paneId || "",
232
+ subtask: assignments[i]?.prompt,
233
+ })),
234
+ ];
205
235
 
206
- return {
207
- sessionName: handle.sessionName,
208
- task,
209
- lead,
210
- agents: assignments.map((a) => a.cli),
211
- layout,
212
- teammateMode: "headless",
213
- startedAt: Date.now(),
214
- members,
215
- headlessResults: results,
216
- handoffs: results.map((r) => ({
217
- paneName: r.paneName,
218
- cli: r.cli,
219
- ...r.handoff,
220
- })),
221
- tasks: buildTasks(
222
- assignments.map((a) => a.prompt),
223
- members.filter((m) => m.role === "worker"),
224
- ),
225
- postSave() {
226
- // headless는 실행 완료 후 즉시 정리 — HUD에 잔존 방지
236
+ return {
237
+ ...state,
238
+ sessionName: handle.sessionName,
239
+ task,
240
+ lead,
241
+ agents: assignments.map((a) => a.cli),
242
+ layout,
243
+ teammateMode: "headless",
244
+ members,
245
+ headlessResults: results,
246
+ handoffs: results.map((r) => ({
247
+ paneName: r.paneName,
248
+ cli: r.cli,
249
+ ...r.handoff,
250
+ })),
251
+ tasks: buildTasks(
252
+ assignments.map((a) => a.prompt),
253
+ members.filter((m) => m.role === "worker"),
254
+ ),
255
+ };
256
+ } finally {
257
+ try {
258
+ handle?.kill();
259
+ } finally {
227
260
  clearTeamState(sessionId);
228
261
  console.log(`\n ${DIM}세션 정리 완료.${RESET}\n`);
229
- },
230
- };
262
+ }
263
+ }
231
264
  }
@@ -20,7 +20,6 @@ export async function startMuxTeam({
20
20
  agents,
21
21
  subtasks,
22
22
  layout,
23
- hubUrl,
24
23
  teammateMode,
25
24
  }) {
26
25
  const paneCount = agents.length + 1;
@@ -64,7 +63,6 @@ export async function startMuxTeam({
64
63
  ok("CLI 초기화 대기 (3초)...");
65
64
  await new Promise((resolve) => setTimeout(resolve, 3000));
66
65
  const promptInjectionFailures = await orchestrate(sessionId, assignments, {
67
- hubUrl,
68
66
  teammateMode,
69
67
  lead: { target: leadTarget, cli: lead, task },
70
68
  onInjectionFailure(failure) {
@@ -89,7 +87,6 @@ export async function startMuxTeam({
89
87
  layout: effectiveLayout,
90
88
  teammateMode,
91
89
  startedAt: Date.now(),
92
- hubUrl,
93
90
  promptInjectionFailures,
94
91
  members,
95
92
  panes: Object.fromEntries(
@@ -1,12 +1,5 @@
1
- import { hasWindowsTerminalSession } from "../../session.mjs";
2
- import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET } from "../../shared.mjs";
3
- import { formatCompletionSuffix } from "../render.mjs";
4
- import { fetchHubTaskList, nativeGetStatus } from "../services/hub-client.mjs";
5
- import {
6
- isNativeMode,
7
- isTeamAlive,
8
- isWtMode,
9
- } from "../services/runtime-mode.mjs";
1
+ import { AMBER, BOLD, DIM, GREEN, RED, RESET } from "../../shared.mjs";
2
+ import { isTeamAlive } from "../services/runtime-mode.mjs";
10
3
  import { loadTeamState } from "../services/state-store.mjs";
11
4
 
12
5
  export async function teamStatus(args = []) {
@@ -41,14 +34,6 @@ export async function teamStatus(args = []) {
41
34
  })),
42
35
  };
43
36
 
44
- if (isNativeMode(state) && alive) {
45
- payload.nativeMembers = (await nativeGetStatus(state))?.data?.members || [];
46
- }
47
-
48
- if (alive) {
49
- payload.hubTasks = await fetchHubTaskList(state);
50
- }
51
-
52
37
  if (json) {
53
38
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
54
39
  return;
@@ -65,11 +50,6 @@ export async function teamStatus(args = []) {
65
50
  ` Uptime: ${alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-"}`,
66
51
  );
67
52
  console.log(` 태스크: ${(state.tasks || []).length}`);
68
- if (isWtMode(state) && !hasWindowsTerminalSession()) {
69
- console.log(
70
- ` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`,
71
- );
72
- }
73
53
 
74
54
  for (const member of state.members || []) {
75
55
  console.log(
@@ -77,40 +57,5 @@ export async function teamStatus(args = []) {
77
57
  );
78
58
  }
79
59
 
80
- if (isNativeMode(state) && alive) {
81
- for (const member of payload.nativeMembers) {
82
- console.log(
83
- ` • ${member.name}: ${member.status}${formatCompletionSuffix(member)}${member.lastPreview ? ` ${DIM}${member.lastPreview}${RESET}` : ""}`,
84
- );
85
- }
86
- }
87
-
88
- if (alive) {
89
- const hubTasks = payload.hubTasks || (await fetchHubTaskList(state));
90
- if (hubTasks.length) {
91
- const completed = hubTasks.filter(
92
- (task) => task.status === "completed",
93
- ).length;
94
- const failed = hubTasks.filter((task) => task.status === "failed").length;
95
- console.log(
96
- `\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`,
97
- );
98
- for (const task of hubTasks) {
99
- const icon =
100
- task.status === "completed"
101
- ? `${GREEN}✓${RESET}`
102
- : task.status === "in_progress"
103
- ? `${AMBER}●${RESET}`
104
- : task.status === "failed"
105
- ? `${RED}✗${RESET}`
106
- : `${GRAY}○${RESET}`;
107
- const owner = task.owner ? ` ${GRAY}[${task.owner}]${RESET}` : "";
108
- console.log(
109
- ` ${icon} ${task.subject || task.description?.slice(0, 50) || ""}${owner}`,
110
- );
111
- }
112
- if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
113
- }
114
- }
115
60
  console.log("");
116
61
  }
@@ -1,31 +1,26 @@
1
- import { closeWtSession, killSession, sessionExists } from "../../session.mjs";
1
+ import { killSession, sessionExists } from "../../session.mjs";
2
2
  import { DIM, RESET } from "../../shared.mjs";
3
3
  import { ok } from "../render.mjs";
4
- import { nativeRequest } from "../services/native-control.mjs";
5
- import { isNativeMode, isWtMode } from "../services/runtime-mode.mjs";
6
4
  import { clearTeamState, loadTeamState } from "../services/state-store.mjs";
7
5
 
8
- export async function teamStop() {
9
- const state = loadTeamState();
10
- if (!state) {
6
+ export async function teamStop(args = []) {
7
+ const state = loadTeamState(args[0]);
8
+ if (!state || (args[0] && state.sessionId !== args[0])) {
11
9
  console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
12
10
  return;
13
11
  }
14
12
 
15
- if (isNativeMode(state)) {
16
- await nativeRequest(state, "/stop", {});
13
+ if (state.teammateMode === "headless") {
14
+ if (!Number.isInteger(state.ownerPid) || state.ownerPid <= 0) {
15
+ throw new Error("headless ownerPid가 없습니다.");
16
+ }
17
17
  try {
18
- process.kill(state.native.supervisorPid, "SIGTERM");
19
- } catch {}
20
- ok(`세션 종료: ${state.sessionName}`);
21
- } else if (isWtMode(state)) {
22
- const closed = closeWtSession({
23
- layout: state?.wt?.layout || state?.layout || "1xN",
24
- paneCount: state?.wt?.paneCount ?? (state.members || []).length,
25
- });
26
- ok(
27
- `세션 종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`,
28
- );
18
+ process.kill(state.ownerPid, "SIGTERM");
19
+ ok(`headless 종료 요청: ${state.sessionId}`);
20
+ } catch (error) {
21
+ if (error.code !== "ESRCH") throw error;
22
+ console.log(` ${DIM}세션 이미 종료됨${RESET}`);
23
+ }
29
24
  } else if (sessionExists(state.sessionName)) {
30
25
  killSession(state.sessionName);
31
26
  ok(`세션 종료: ${state.sessionName}`);
@@ -4,18 +4,17 @@ export function renderTeamHelp() {
4
4
  console.log(`
5
5
  ${AMBER}${BOLD}⬡ tfx multi${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
6
6
 
7
- ${BOLD}시작${RESET}
7
+ ${BOLD}Usage${RESET}
8
8
  ${WHITE}tfx multi "작업 설명"${RESET}
9
9
  ${WHITE}tfx multi --agents codex,antigravity --lead claude "작업"${RESET}
10
10
  ${WHITE}tfx multi --teammate-mode tmux "작업"${RESET}
11
- ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
11
+ ${WHITE}tfx multi --teammate-mode headless "작업"${RESET}
12
12
  ${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
13
13
  ${WHITE}tfx multi --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
14
14
  ${WHITE}tfx multi --dashboard-layout lite "작업"${RESET} ${DIM}(dashboard-lite 기본 뷰)${RESET}
15
15
  ${WHITE}tfx multi --dashboard-layout auto "작업"${RESET} ${DIM}(dashboard viewer 레이아웃 자동 결정)${RESET}
16
- ${WHITE}tfx multi --dashboard-size 0.4 "작업"${RESET} ${DIM}(대시보드 분할 비율 0.2~0.8, 기본 0.50)${RESET}
16
+ ${WHITE}tfx multi --dashboard-size 0.4 "작업"${RESET} ${DIM}(대시보드 분할 비율 0.2~0.8, 기본 0.40)${RESET}
17
17
  ${WHITE}tfx multi --dashboard-anchor window "작업"${RESET} ${DIM}(대시보드 고정 위치: window|tab, 기본 window)${RESET}
18
- ${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
19
18
 
20
19
  ${BOLD}제어${RESET}
21
20
  ${WHITE}tfx multi status${RESET} ${GRAY}현재 팀 상태${RESET}
@@ -27,7 +26,7 @@ export function renderTeamHelp() {
27
26
  ${WHITE}tfx multi send${RESET} ${DIM}<lead|이름|번호> "msg"${RESET} ${GRAY}팀메이트에 메시지 주입${RESET}
28
27
  ${WHITE}tfx multi interrupt${RESET} ${DIM}<대상>${RESET} ${GRAY}팀메이트 인터럽트(C-c)${RESET}
29
28
  ${WHITE}tfx multi control${RESET} ${DIM}<대상> <cmd>${RESET} ${GRAY}리드 제어명령(interrupt|stop|pause|resume)${RESET}
30
- ${WHITE}tfx multi stop${RESET} ${GRAY}graceful 종료${RESET}
29
+ ${WHITE}tfx multi stop${RESET} ${DIM}[session-id]${RESET} ${GRAY}선택한 팀 세션 종료${RESET}
31
30
  ${WHITE}tfx multi kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
32
31
  ${WHITE}tfx multi list${RESET} ${GRAY}활성 세션 목록${RESET}
33
32
 
@@ -32,7 +32,11 @@ export const TEAM_COMMANDS = [
32
32
  usage: "tfx multi control <대상> <cmd> [사유]",
33
33
  desc: "리드 제어명령 전송",
34
34
  },
35
- { name: "stop", usage: "tfx multi stop", desc: "graceful 종료" },
35
+ {
36
+ name: "stop",
37
+ usage: "tfx multi stop [session-id]",
38
+ desc: "선택한 팀 세션 종료",
39
+ },
36
40
  { name: "kill", usage: "tfx multi kill", desc: "모든 팀 세션 강제 종료" },
37
41
  { name: "list", usage: "tfx multi list", desc: "활성 세션 목록" },
38
42
  { name: "help", usage: "tfx multi help", desc: "도움말" },
@@ -38,15 +38,3 @@ export function renderTasks(tasks = []) {
38
38
  }
39
39
  console.log("");
40
40
  }
41
-
42
- export function formatCompletionSuffix(member) {
43
- if (!member?.completionStatus) return "";
44
- if (member.completionStatus === "abnormal") {
45
- return ` ${RED}[abnormal:${member.completionReason || "unknown"}]${RESET}`;
46
- }
47
- if (member.completionStatus === "normal")
48
- return ` ${GREEN}[route-ok]${RESET}`;
49
- if (member.completionStatus === "unchecked")
50
- return ` ${GRAY}[route-unchecked]${RESET}`;
51
- return "";
52
- }
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import {
3
2
  existsSync,
4
3
  mkdirSync,
@@ -7,15 +6,12 @@ import {
7
6
  writeFileSync,
8
7
  } from "node:fs";
9
8
  import { join } from "node:path";
10
- import { resolveHubPortForContext } from "../../../hub-lifecycle.mjs";
11
9
  import { publishLeadControl as publishLeadControlBridge } from "../../lead-control.mjs";
12
10
  import {
13
11
  getTeamStatus as fetchTeamStatus,
14
12
  subscribeToLeadCommands as pullLeadCommands,
15
13
  } from "../../session-sync.mjs";
16
- import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
17
-
18
- export { nativeGetStatus } from "./native-control.mjs";
14
+ import { HUB_PID_DIR } from "./state-store.mjs";
19
15
 
20
16
  const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
21
17
  const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
@@ -109,91 +105,9 @@ export async function getHubInfo() {
109
105
  return null;
110
106
  }
111
107
 
112
- export async function startHubDaemon() {
113
- const serverPath = join(PKG_ROOT, "hub", "server.mjs");
114
- if (!existsSync(serverPath)) {
115
- const error = new Error("hub/server.mjs 없음");
116
- error.code = "HUB_SERVER_MISSING";
117
- throw error;
118
- }
119
-
120
- // Resolve the canonical port. server.mjs forces 27888 in worktree/ephemeral
121
- // contexts via resolveHubPortForContext; mirror that here so a polluted
122
- // TFX_HUB_PORT does not make us spawn with / probe a port the daemon never
123
- // binds (defense-in-depth — do not rely solely on server.mjs's boundary guard).
124
- const resolvedPort = resolveHubPortForContext({
125
- env: process.env,
126
- cwd: process.cwd(),
127
- });
128
-
129
- const child = spawn(process.execPath, [serverPath], {
130
- env: { ...process.env, TFX_HUB_PORT: String(resolvedPort) },
131
- stdio: "ignore",
132
- detached: true,
133
- windowsHide: true,
134
- });
135
- child.unref();
136
-
137
- const expectedPort = resolvedPort;
138
- const deadline = Date.now() + 3000;
139
- while (Date.now() < deadline) {
140
- const status = await probeHubStatus("127.0.0.1", expectedPort, 500);
141
- if (status?.hub) {
142
- return {
143
- pid: Number(status.pid) || child.pid,
144
- host: "127.0.0.1",
145
- port: expectedPort,
146
- url: `${buildHubBaseUrl("127.0.0.1", expectedPort)}/mcp`,
147
- };
148
- }
149
- await new Promise((resolve) => setTimeout(resolve, 100));
150
- }
151
-
152
- return null;
153
- }
154
-
155
- /**
156
- * Hub가 살아있는지 확인하고, 죽어있으면 재시작을 시도한다.
157
- * exponential backoff: 1초, 2초, 4초
158
- * 모든 재시작 실패 시 에러를 throw한다 (silent fail 아님).
159
- * @param {number} [maxRetries=3]
160
- * @returns {Promise<object>} Hub 정보
161
- * @throws {Error} 모든 재시작 시도 실패 시
162
- */
163
- export async function ensureHubAlive(maxRetries = 3) {
164
- const hub = await getHubInfo();
165
- if (hub && !hub.degraded) return hub;
166
-
167
- let lastError = null;
168
- for (let i = 0; i < maxRetries; i++) {
169
- try {
170
- const restarted = await startHubDaemon();
171
- if (restarted) {
172
- // 재시작 후 연결 복구 확인
173
- const recovered = await getHubInfo();
174
- if (recovered) return recovered;
175
- }
176
- } catch (err) {
177
- lastError = err;
178
- }
179
- // 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
180
- if (i < maxRetries - 1) {
181
- const backoffMs = 2 ** i * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
182
- await new Promise((resolve) => setTimeout(resolve, backoffMs));
183
- }
184
- }
185
-
186
- const error = new Error(
187
- `Hub 재시작 ${maxRetries}회 모두 실패${lastError ? `: ${lastError.message}` : ""}`,
188
- );
189
- error.code = "HUB_RESTART_FAILED";
190
- error.cause = lastError;
191
- throw error;
192
- }
193
-
194
108
  export async function fetchHubTaskList(state) {
195
109
  const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
196
- const teamName = state?.native?.teamName || state?.sessionName || null;
110
+ const teamName = state?.sessionName || null;
197
111
  if (!teamName) return [];
198
112
 
199
113
  try {