@triflux/remote 10.0.1 → 10.25.1
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/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +68 -7
- package/hub/server.mjs +2181 -701
- package/hub/store-adapter.mjs +101 -37
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +127 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +45 -17
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +21 -119
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1067 -264
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +69 -53
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +62 -34
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1526 -198
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +21 -37
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -45
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +936 -449
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +518 -64
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +663 -337
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
4
10
|
|
|
5
11
|
import { publishLeadControl as publishLeadControlBridge } from "../../lead-control.mjs";
|
|
6
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getTeamStatus as fetchTeamStatus,
|
|
14
|
+
subscribeToLeadCommands as pullLeadCommands,
|
|
15
|
+
} from "../../session-sync.mjs";
|
|
7
16
|
import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
|
|
17
|
+
|
|
8
18
|
export { nativeGetStatus } from "./native-control.mjs";
|
|
9
19
|
|
|
10
20
|
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
@@ -56,18 +66,20 @@ export async function getHubInfo() {
|
|
|
56
66
|
if (!Number.isFinite(pid) || pid <= 0) throw new Error("invalid pid");
|
|
57
67
|
process.kill(pid, 0);
|
|
58
68
|
const host = normalizeLoopbackHost(raw?.host);
|
|
59
|
-
const port = Number(raw?.port) ||
|
|
69
|
+
const port = Number(raw?.port) || probePort;
|
|
60
70
|
const status = await probeHubStatus(host, port, 1200);
|
|
71
|
+
if (!status) throw new Error("pid file process is not a healthy hub");
|
|
61
72
|
return {
|
|
62
73
|
...raw,
|
|
63
74
|
pid,
|
|
64
75
|
host,
|
|
65
76
|
port,
|
|
66
77
|
url: `${buildHubBaseUrl(host, port)}/mcp`,
|
|
67
|
-
...(status ? {} : { degraded: true }),
|
|
68
78
|
};
|
|
69
79
|
} catch {
|
|
70
|
-
try {
|
|
80
|
+
try {
|
|
81
|
+
unlinkSync(HUB_PID_FILE);
|
|
82
|
+
} catch {}
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
@@ -86,7 +98,10 @@ export async function getHubInfo() {
|
|
|
86
98
|
if (Number.isFinite(recovered.pid) && recovered.pid > 0) {
|
|
87
99
|
try {
|
|
88
100
|
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
89
|
-
writeFileSync(
|
|
101
|
+
writeFileSync(
|
|
102
|
+
HUB_PID_FILE,
|
|
103
|
+
JSON.stringify({ ...recovered, started: Date.now() }),
|
|
104
|
+
);
|
|
90
105
|
} catch {}
|
|
91
106
|
}
|
|
92
107
|
return recovered;
|
|
@@ -154,12 +169,14 @@ export async function ensureHubAlive(maxRetries = 3) {
|
|
|
154
169
|
}
|
|
155
170
|
// 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
|
|
156
171
|
if (i < maxRetries - 1) {
|
|
157
|
-
const backoffMs =
|
|
172
|
+
const backoffMs = 2 ** i * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
|
|
158
173
|
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
159
174
|
}
|
|
160
175
|
}
|
|
161
176
|
|
|
162
|
-
const error = new Error(
|
|
177
|
+
const error = new Error(
|
|
178
|
+
`Hub 재시작 ${maxRetries}회 모두 실패${lastError ? `: ${lastError.message}` : ""}`,
|
|
179
|
+
);
|
|
163
180
|
error.code = "HUB_RESTART_FAILED";
|
|
164
181
|
error.cause = lastError;
|
|
165
182
|
throw error;
|
|
@@ -178,16 +195,24 @@ export async function fetchHubTaskList(state) {
|
|
|
178
195
|
signal: AbortSignal.timeout(2000),
|
|
179
196
|
});
|
|
180
197
|
const data = await res.json();
|
|
181
|
-
return data?.ok ?
|
|
198
|
+
return data?.ok ? data.data?.tasks || [] : [];
|
|
182
199
|
} catch {
|
|
183
200
|
return [];
|
|
184
201
|
}
|
|
185
202
|
}
|
|
186
203
|
|
|
187
|
-
export async function publishLeadControl(
|
|
204
|
+
export async function publishLeadControl(
|
|
205
|
+
state,
|
|
206
|
+
targetMember,
|
|
207
|
+
command,
|
|
208
|
+
reason = "",
|
|
209
|
+
) {
|
|
188
210
|
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
189
|
-
const leadAgent =
|
|
190
|
-
|
|
211
|
+
const leadAgent =
|
|
212
|
+
(state?.members || []).find((member) => member.role === "lead")?.agentId ||
|
|
213
|
+
"lead";
|
|
214
|
+
const targetAgent =
|
|
215
|
+
typeof targetMember === "string" ? targetMember : targetMember?.agentId;
|
|
191
216
|
|
|
192
217
|
const result = await publishLeadControlBridge({
|
|
193
218
|
hubUrl: hubBase,
|
|
@@ -206,10 +231,13 @@ export async function publishLeadControl(state, targetMember, command, reason =
|
|
|
206
231
|
|
|
207
232
|
export async function subscribeToLeadCommands(state, member, options = {}) {
|
|
208
233
|
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
209
|
-
const fallbackAgentId =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
234
|
+
const fallbackAgentId =
|
|
235
|
+
(state?.members || []).find((candidate) => candidate.role === "lead")
|
|
236
|
+
?.agentId || null;
|
|
237
|
+
const agentId =
|
|
238
|
+
typeof member === "string"
|
|
239
|
+
? member
|
|
240
|
+
: member?.agentId || options?.agentId || fallbackAgentId;
|
|
213
241
|
|
|
214
242
|
return await pullLeadCommands({
|
|
215
243
|
hubUrl: hubBase,
|
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
export function resolveMember(state, selector) {
|
|
2
|
-
const members = state?.members || [];
|
|
3
|
-
if (!selector) return null;
|
|
4
|
-
|
|
5
|
-
const direct = members.find(
|
|
6
|
-
member
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
export function resolveMember(state, selector) {
|
|
2
|
+
const members = state?.members || [];
|
|
3
|
+
if (!selector) return null;
|
|
4
|
+
|
|
5
|
+
const direct = members.find(
|
|
6
|
+
(member) =>
|
|
7
|
+
member.name === selector ||
|
|
8
|
+
member.role === selector ||
|
|
9
|
+
member.agentId === selector,
|
|
10
|
+
);
|
|
11
|
+
if (direct) return direct;
|
|
12
|
+
|
|
13
|
+
const workerAlias = /^worker-(\d+)$/i.exec(selector);
|
|
14
|
+
if (workerAlias) {
|
|
15
|
+
const index = parseInt(workerAlias[1], 10) - 1;
|
|
16
|
+
const workers = members.filter((member) => member.role === "worker");
|
|
17
|
+
if (index >= 0 && index < workers.length) return workers[index];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const numeric = parseInt(selector, 10);
|
|
21
|
+
if (!Number.isNaN(numeric)) {
|
|
22
|
+
const byPane = members.find(
|
|
23
|
+
(member) =>
|
|
24
|
+
member.pane?.endsWith(`.${numeric}`) ||
|
|
25
|
+
member.pane?.endsWith(`:${numeric}`),
|
|
26
|
+
);
|
|
27
|
+
if (byPane) return byPane;
|
|
28
|
+
if (numeric >= 1 && numeric <= members.length) return members[numeric - 1];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function toAgentId(cli, target) {
|
|
35
|
+
const suffix = String(target).split(/[:.]/).pop();
|
|
36
|
+
return `${cli}-${suffix}`;
|
|
37
|
+
}
|
|
@@ -1,117 +1,145 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
|
|
6
|
-
import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
case "
|
|
13
|
-
return
|
|
14
|
-
case "
|
|
15
|
-
return "
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { buildExecArgs } from "@triflux/core/hub/codex-adapter.mjs";
|
|
5
|
+
import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
|
|
6
|
+
import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
|
|
7
|
+
|
|
8
|
+
export function buildNativeCliCommand(cli) {
|
|
9
|
+
switch (cli) {
|
|
10
|
+
case "codex":
|
|
11
|
+
return buildExecArgs({});
|
|
12
|
+
case "gemini":
|
|
13
|
+
return "gemini";
|
|
14
|
+
case "claude":
|
|
15
|
+
return "claude";
|
|
16
|
+
default:
|
|
17
|
+
return cli;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function startNativeSupervisor({
|
|
22
|
+
sessionId,
|
|
23
|
+
task,
|
|
24
|
+
lead,
|
|
25
|
+
agents,
|
|
26
|
+
subtasks,
|
|
27
|
+
hubUrl,
|
|
28
|
+
}) {
|
|
29
|
+
const configPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
|
|
30
|
+
const runtimePath = join(
|
|
31
|
+
HUB_PID_DIR,
|
|
32
|
+
`team-native-${sessionId}.runtime.json`,
|
|
33
|
+
);
|
|
34
|
+
const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
|
|
35
|
+
mkdirSync(logsDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const leadMember = {
|
|
38
|
+
role: "lead",
|
|
39
|
+
name: "lead",
|
|
40
|
+
cli: lead,
|
|
41
|
+
agentId: `${lead}-lead`,
|
|
42
|
+
command: buildNativeCliCommand(lead),
|
|
43
|
+
};
|
|
44
|
+
const workers = agents.map((cli, index) => ({
|
|
45
|
+
role: "worker",
|
|
46
|
+
name: `${cli}-${index + 1}`,
|
|
47
|
+
cli,
|
|
48
|
+
agentId: `${cli}-w${index + 1}`,
|
|
49
|
+
command: buildNativeCliCommand(cli),
|
|
50
|
+
subtask: subtasks[index],
|
|
51
|
+
}));
|
|
52
|
+
const members = [
|
|
53
|
+
{
|
|
54
|
+
...leadMember,
|
|
55
|
+
prompt: buildLeadPrompt(task, {
|
|
56
|
+
agentId: leadMember.agentId,
|
|
57
|
+
hubUrl,
|
|
58
|
+
teammateMode: "in-process",
|
|
59
|
+
workers: workers.map((worker) => ({
|
|
60
|
+
agentId: worker.agentId,
|
|
61
|
+
cli: worker.cli,
|
|
62
|
+
subtask: worker.subtask,
|
|
63
|
+
})),
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
...workers.map((worker) => ({
|
|
67
|
+
...worker,
|
|
68
|
+
prompt: buildPrompt(worker.subtask, {
|
|
69
|
+
cli: worker.cli,
|
|
70
|
+
agentId: worker.agentId,
|
|
71
|
+
hubUrl,
|
|
72
|
+
}),
|
|
73
|
+
})),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
writeFileSync(
|
|
77
|
+
configPath,
|
|
78
|
+
JSON.stringify(
|
|
79
|
+
{
|
|
80
|
+
sessionName: sessionId,
|
|
81
|
+
hubUrl,
|
|
82
|
+
workdir: process.cwd(),
|
|
83
|
+
startupDelayMs: 3000,
|
|
84
|
+
logsDir,
|
|
85
|
+
runtimeFile: runtimePath,
|
|
86
|
+
members,
|
|
87
|
+
},
|
|
88
|
+
null,
|
|
89
|
+
2,
|
|
90
|
+
) + "\n",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const child = spawn(
|
|
94
|
+
process.execPath,
|
|
95
|
+
[
|
|
96
|
+
join(PKG_ROOT, "hub", "team", "native-supervisor.mjs"),
|
|
97
|
+
"--config",
|
|
98
|
+
configPath,
|
|
99
|
+
],
|
|
100
|
+
{
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: "ignore",
|
|
103
|
+
env: { ...process.env },
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
child.unref();
|
|
108
|
+
|
|
109
|
+
const deadline = Date.now() + 5000;
|
|
110
|
+
while (Date.now() < deadline) {
|
|
111
|
+
if (existsSync(runtimePath)) {
|
|
112
|
+
try {
|
|
113
|
+
const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
|
|
114
|
+
return { runtime, members };
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { runtime: null, members };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function nativeRequest(state, path, body = {}) {
|
|
124
|
+
if (!state?.native?.controlUrl) return null;
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`${state.native.controlUrl}${path}`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify(body),
|
|
130
|
+
});
|
|
131
|
+
return await res.json();
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function nativeGetStatus(state) {
|
|
138
|
+
if (!state?.native?.controlUrl) return null;
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch(`${state.native.controlUrl}/status`);
|
|
141
|
+
return await res.json();
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
117
145
|
}
|
|
@@ -22,7 +22,8 @@ export function normalizeTeammateMode(mode = "auto") {
|
|
|
22
22
|
export function normalizeLayout(layout = "2x2") {
|
|
23
23
|
const raw = String(layout).toLowerCase();
|
|
24
24
|
if (raw === "2x2" || raw === "grid") return "2x2";
|
|
25
|
-
if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns")
|
|
25
|
+
if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns")
|
|
26
|
+
return "1xN";
|
|
26
27
|
if (raw === "nx1" || raw === "horizontal" || raw === "rows") return "Nx1";
|
|
27
28
|
return "2x2";
|
|
28
29
|
}
|
|
@@ -60,3 +61,56 @@ export function ensureTmuxOrExit() {
|
|
|
60
61
|
error.code = "TMUX_REQUIRED";
|
|
61
62
|
throw error;
|
|
62
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* teammateMode + 환경 기반 effective mode 결정 (pure function).
|
|
67
|
+
*
|
|
68
|
+
* - wt → wt.exe 없으면 in-process
|
|
69
|
+
* - wt → WT_SESSION 없으면 in-process
|
|
70
|
+
* - in-process → non-TTY 감지 시 headless (#114: run_in_background 부모 종료 시 자식 SIGTERM 방지)
|
|
71
|
+
* - override: TFX_FORCE_IN_PROCESS=1 로 in-process 유지
|
|
72
|
+
*
|
|
73
|
+
* @param {string} teammateMode
|
|
74
|
+
* @param {{
|
|
75
|
+
* hasWt?: () => boolean,
|
|
76
|
+
* hasWtSession?: () => boolean,
|
|
77
|
+
* isTTY?: boolean,
|
|
78
|
+
* env?: Record<string,string|undefined>,
|
|
79
|
+
* }} [deps]
|
|
80
|
+
* @returns {{ mode: string, warnings: string[] }}
|
|
81
|
+
*/
|
|
82
|
+
export function resolveEffectiveMode(teammateMode, deps = {}) {
|
|
83
|
+
const hasWt = deps.hasWt || hasWindowsTerminal;
|
|
84
|
+
const hasWtSession = deps.hasWtSession || hasWindowsTerminalSession;
|
|
85
|
+
const isTTY =
|
|
86
|
+
typeof deps.isTTY === "boolean"
|
|
87
|
+
? deps.isTTY
|
|
88
|
+
: Boolean(process.stdout.isTTY);
|
|
89
|
+
const env = deps.env || process.env;
|
|
90
|
+
|
|
91
|
+
const warnings = [];
|
|
92
|
+
let effective = teammateMode;
|
|
93
|
+
|
|
94
|
+
if (effective === "wt" && !hasWt()) {
|
|
95
|
+
warnings.push("wt.exe 미발견 — in-process 모드로 자동 fallback");
|
|
96
|
+
effective = "in-process";
|
|
97
|
+
}
|
|
98
|
+
if (effective === "wt" && !hasWtSession()) {
|
|
99
|
+
warnings.push(
|
|
100
|
+
"WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback",
|
|
101
|
+
);
|
|
102
|
+
effective = "in-process";
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
effective === "in-process" &&
|
|
106
|
+
!isTTY &&
|
|
107
|
+
env.TFX_FORCE_IN_PROCESS !== "1"
|
|
108
|
+
) {
|
|
109
|
+
warnings.push(
|
|
110
|
+
"non-TTY 환경 감지 (run_in_background 등) — headless 모드로 자동 fallback (#114). 강제 유지: TFX_FORCE_IN_PROCESS=1",
|
|
111
|
+
);
|
|
112
|
+
effective = "headless";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { mode: effective, warnings };
|
|
116
|
+
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
statSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
4
10
|
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
5
12
|
import { fileURLToPath } from "node:url";
|
|
6
13
|
|
|
7
14
|
export const PKG_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
8
|
-
export const HUB_PID_DIR =
|
|
15
|
+
export const HUB_PID_DIR =
|
|
16
|
+
process.env.TFX_HUB_PID_DIR || join(homedir(), ".claude", "cache", "tfx-hub");
|
|
9
17
|
export const TEAM_PROFILE = (() => {
|
|
10
|
-
const raw = String(process.env.TFX_TEAM_PROFILE || "team")
|
|
18
|
+
const raw = String(process.env.TFX_TEAM_PROFILE || "team")
|
|
19
|
+
.trim()
|
|
20
|
+
.toLowerCase();
|
|
11
21
|
return raw === "codex-team" ? "codex-team" : "team";
|
|
12
22
|
})();
|
|
13
23
|
|
|
@@ -15,31 +25,78 @@ export const SESSION_ID = process.env.CLAUDE_SESSION_ID || `s${Date.now()}`;
|
|
|
15
25
|
|
|
16
26
|
function getStatePath(sessionId) {
|
|
17
27
|
if (sessionId) return join(HUB_PID_DIR, `team-state-${sessionId}.json`);
|
|
18
|
-
return join(
|
|
28
|
+
return join(
|
|
29
|
+
HUB_PID_DIR,
|
|
30
|
+
TEAM_PROFILE === "codex-team"
|
|
31
|
+
? "team-state-codex-team.json"
|
|
32
|
+
: "team-state.json",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findLatestMatchingTeamState() {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(HUB_PID_DIR)) return null;
|
|
39
|
+
const entries = readdirSync(HUB_PID_DIR)
|
|
40
|
+
.filter((name) => /^team-state-.+\.json$/.test(name))
|
|
41
|
+
.map((name) => {
|
|
42
|
+
const path = join(HUB_PID_DIR, name);
|
|
43
|
+
try {
|
|
44
|
+
return { path, mtime: statSync(path).mtimeMs };
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
51
|
+
for (const { path } of entries) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
54
|
+
if ((parsed.profile || "team") === TEAM_PROFILE) return parsed;
|
|
55
|
+
} catch {
|
|
56
|
+
// corrupt entry — skip
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// directory walk failure
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
19
63
|
}
|
|
20
64
|
|
|
21
65
|
export function loadTeamState(sessionId) {
|
|
66
|
+
const explicit = Boolean(sessionId);
|
|
22
67
|
const resolvedId = sessionId || SESSION_ID;
|
|
23
68
|
const sessionPath = getStatePath(resolvedId);
|
|
24
69
|
try {
|
|
25
|
-
if (existsSync(sessionPath))
|
|
70
|
+
if (existsSync(sessionPath))
|
|
71
|
+
return JSON.parse(readFileSync(sessionPath, "utf8"));
|
|
26
72
|
} catch {
|
|
27
73
|
return null;
|
|
28
74
|
}
|
|
29
75
|
// 세션별 파일 없으면 기존 team-state.json fallback
|
|
30
76
|
const legacyPath = getStatePath(null);
|
|
31
77
|
try {
|
|
32
|
-
if (existsSync(legacyPath))
|
|
78
|
+
if (existsSync(legacyPath))
|
|
79
|
+
return JSON.parse(readFileSync(legacyPath, "utf8"));
|
|
33
80
|
} catch {
|
|
34
81
|
return null;
|
|
35
82
|
}
|
|
83
|
+
// Issue #116-B: CLAUDE_SESSION_ID 미설정이거나 bg shell 의 다른 세션이 저장한
|
|
84
|
+
// team-state-*.json 을 tfx multi status 가 못 찾던 false-negative 우회.
|
|
85
|
+
// 명시 sessionId lookup 은 의도적 조회이므로 auto-discover 하지 않는다.
|
|
86
|
+
if (!explicit) {
|
|
87
|
+
const latest = findLatestMatchingTeamState();
|
|
88
|
+
if (latest) return latest;
|
|
89
|
+
}
|
|
36
90
|
return null;
|
|
37
91
|
}
|
|
38
92
|
|
|
39
93
|
export function saveTeamState(state, sessionId) {
|
|
40
94
|
const path = getStatePath(sessionId || state.sessionId || SESSION_ID);
|
|
41
95
|
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
-
writeFileSync(
|
|
96
|
+
writeFileSync(
|
|
97
|
+
path,
|
|
98
|
+
JSON.stringify({ ...state, profile: TEAM_PROFILE }, null, 2) + "\n",
|
|
99
|
+
);
|
|
43
100
|
}
|
|
44
101
|
|
|
45
102
|
export function clearTeamState(sessionId) {
|