@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.
- package/cto/brief.mjs +0 -12
- package/cto/collect.mjs +16 -247
- package/cto/current.schema.json +4 -20
- package/cto/events.mjs +13 -294
- package/cto/hygiene.mjs +26 -346
- package/cto/lake-root.mjs +114 -16
- package/cto/status.mjs +33 -128
- package/hub/index.mjs +0 -8
- package/hub/store-adapter.mjs +1 -1
- package/hub/team/agent-map.json +0 -1
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +1 -37
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +41 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +0 -3
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/hub-client.mjs +2 -88
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +14 -755
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +16 -403
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session.mjs +8 -119
- package/hub/team/swarm-intent.mjs +0 -198
- package/hub/team/terminal-opener.mjs +11 -35
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +8 -269
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +4 -37
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/skill-template.mjs +0 -271
- package/cto/hygiene-actions.mjs +0 -355
- package/cto/hygiene-notify.mjs +0 -170
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/gemini-worker.mjs +0 -217
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -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
|
|
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 || "
|
|
106
|
+
`[headless] Claude native bridge mode: ${nativeBridgeMode || "agents"}\n`,
|
|
84
107
|
);
|
|
85
108
|
}
|
|
86
109
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
170
|
-
if (verbose) {
|
|
184
|
+
// handoff 요약 (Lead 토큰 절약 포맷)
|
|
171
185
|
for (const r of results) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
console.log(` ${
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
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 (
|
|
16
|
-
|
|
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.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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}`);
|
package/hub/team/cli/help.mjs
CHANGED
|
@@ -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}
|
|
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
|
|
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.
|
|
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}
|
|
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
|
-
{
|
|
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: "도움말" },
|
package/hub/team/cli/render.mjs
CHANGED
|
@@ -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
|
|
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?.
|
|
110
|
+
const teamName = state?.sessionName || null;
|
|
197
111
|
if (!teamName) return [];
|
|
198
112
|
|
|
199
113
|
try {
|