@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,145 +0,0 @@
1
- const TASK_TYPES = Object.freeze([
2
- "implement",
3
- "review",
4
- "research",
5
- "qa",
6
- "ship",
7
- "multi",
8
- "swarm",
9
- ]);
10
-
11
- const CLI_TYPES = Object.freeze(["codex", "gemini"]);
12
-
13
- export const MCP_CATALOG = Object.freeze([
14
- Object.freeze({
15
- name: "filesystem",
16
- purpose: "Read and write workspace files",
17
- taskTypes: Object.freeze(["implement", "review", "qa", "ship"]),
18
- cli: Object.freeze(["codex", "gemini"]),
19
- }),
20
- Object.freeze({
21
- name: "github",
22
- purpose: "Inspect PRs, issues, and release state",
23
- taskTypes: Object.freeze(["review", "ship"]),
24
- cli: Object.freeze(["codex"]),
25
- }),
26
- Object.freeze({
27
- name: "browser",
28
- purpose: "Research web pages and verify browser flows",
29
- taskTypes: Object.freeze(["research", "qa"]),
30
- cli: Object.freeze(["codex", "gemini"]),
31
- }),
32
- Object.freeze({
33
- name: "context7",
34
- purpose: "Fetch current SDK and API documentation",
35
- taskTypes: Object.freeze(["implement", "review", "research"]),
36
- cli: Object.freeze(["codex", "gemini"]),
37
- }),
38
- Object.freeze({
39
- name: "exa",
40
- purpose: "Search code examples and repositories",
41
- taskTypes: Object.freeze(["research", "review"]),
42
- cli: Object.freeze(["codex", "gemini"]),
43
- }),
44
- Object.freeze({
45
- name: "tavily",
46
- purpose: "Verify current external facts and search results",
47
- taskTypes: Object.freeze(["research", "qa"]),
48
- cli: Object.freeze(["gemini"]),
49
- }),
50
- Object.freeze({
51
- name: "sequential-thinking",
52
- purpose: "Structured reasoning for audits and review",
53
- taskTypes: Object.freeze(["review"]),
54
- cli: Object.freeze(["codex"]),
55
- }),
56
- Object.freeze({
57
- name: "tfx-hub",
58
- purpose: "Coordinate triflux hub, multi-agent, and swarm work",
59
- taskTypes: Object.freeze(["ship", "multi", "swarm"]),
60
- cli: Object.freeze(["codex", "gemini"]),
61
- }),
62
- ]);
63
-
64
- function uniqueStrings(values = []) {
65
- return [
66
- ...new Set(
67
- (Array.isArray(values) ? values : [])
68
- .map((value) => String(value ?? "").trim())
69
- .filter(Boolean),
70
- ),
71
- ];
72
- }
73
-
74
- function assertOneOf(label, value, allowed) {
75
- if (!allowed.includes(value)) {
76
- throw new TypeError(`${label} must be one of: ${allowed.join(", ")}`);
77
- }
78
- }
79
-
80
- export function selectMcpServers(opts = {}) {
81
- const taskType = String(opts.taskType ?? "").trim();
82
- const cli = String(opts.cli ?? "").trim();
83
- assertOneOf("taskType", taskType, TASK_TYPES);
84
- assertOneOf("cli", cli, CLI_TYPES);
85
-
86
- const allCatalogNames = MCP_CATALOG.map((server) => server.name);
87
- const available = new Set(
88
- uniqueStrings(opts.available?.length ? opts.available : allCatalogNames),
89
- );
90
- const exclude = new Set(uniqueStrings(opts.exclude));
91
- const force = uniqueStrings(opts.force);
92
-
93
- const taskMatched = MCP_CATALOG.filter((server) =>
94
- server.taskTypes.includes(taskType),
95
- );
96
- const compatible = taskMatched.filter((server) => server.cli.includes(cli));
97
- const incompatible = taskMatched.filter(
98
- (server) => !server.cli.includes(cli),
99
- );
100
-
101
- const selected = uniqueStrings([
102
- ...compatible
103
- .map((server) => server.name)
104
- .filter((name) => available.has(name) && !exclude.has(name)),
105
- ...force.filter((name) => available.has(name) && !exclude.has(name)),
106
- ]);
107
-
108
- const reasonParts = [
109
- `task=${taskType}`,
110
- `cli=${cli}`,
111
- `selected=${selected.length ? selected.join(", ") : "none"}`,
112
- ];
113
- if (incompatible.length) {
114
- reasonParts.push(
115
- `cli-filtered=${incompatible.map((server) => server.name).join(", ")}`,
116
- );
117
- }
118
- if (force.length) reasonParts.push(`forced=${force.join(", ")}`);
119
- if (exclude.size) reasonParts.push(`excluded=${[...exclude].join(", ")}`);
120
- if (opts.available?.length) {
121
- reasonParts.push(`available=${[...available].join(", ")}`);
122
- }
123
-
124
- return {
125
- selected,
126
- reason: reasonParts.join(" | "),
127
- };
128
- }
129
-
130
- export function buildMcpArgs(servers = [], cli) {
131
- const resolvedCli = String(cli ?? "").trim();
132
- assertOneOf("cli", resolvedCli, CLI_TYPES);
133
-
134
- const selected = uniqueStrings(servers);
135
- if (!selected.length) return [];
136
-
137
- if (resolvedCli === "codex") {
138
- return selected.flatMap((server) => [
139
- "-c",
140
- `mcp_servers.${server}.enabled=true`,
141
- ]);
142
- }
143
-
144
- return ["--allowed-mcp-server-names", ...selected];
145
- }
@@ -1,460 +0,0 @@
1
- // hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
2
-
3
- import { execSync as execSyncSupervisor, spawn } from "node:child_process";
4
- import {
5
- createWriteStream,
6
- mkdirSync,
7
- readFileSync,
8
- writeFileSync,
9
- } from "node:fs";
10
- import { createServer } from "node:http";
11
- import { dirname, join } from "node:path";
12
- import {
13
- ensureBashScriptExecution,
14
- resolveBashExecutable,
15
- } from "@triflux/core/hub/lib/bash-path.mjs";
16
- import { verifySlimWrapperRouteExecution } from "./native.mjs";
17
- import { forceCleanupTeam } from "./nativeProxy.mjs";
18
- import { buildWorkerSandboxEnv } from "./worker-sandbox.mjs";
19
-
20
- const ROUTE_LOG_TAIL_BYTES = 65536;
21
-
22
- function parseArgs(argv) {
23
- const out = {};
24
- for (let i = 0; i < argv.length; i++) {
25
- const cur = argv[i];
26
- if (cur === "--config" && argv[i + 1]) {
27
- out.config = argv[++i];
28
- }
29
- }
30
- return out;
31
- }
32
-
33
- async function readJson(path) {
34
- return JSON.parse(readFileSync(path, "utf8"));
35
- }
36
-
37
- function safeText(v, fallback = "") {
38
- if (v == null) return fallback;
39
- return String(v);
40
- }
41
-
42
- function readTailText(path, maxBytes = ROUTE_LOG_TAIL_BYTES) {
43
- try {
44
- const raw = readFileSync(path, "utf8");
45
- if (raw.length <= maxBytes) return raw;
46
- return raw.slice(-maxBytes);
47
- } catch {
48
- return "";
49
- }
50
- }
51
-
52
- function finalizeRouteVerification(state) {
53
- if (state?.member?.role !== "worker") return;
54
-
55
- const verification = verifySlimWrapperRouteExecution({
56
- promptText: safeText(state.member?.prompt),
57
- stdoutText: readTailText(state.logFile),
58
- stderrText: readTailText(state.errFile),
59
- });
60
-
61
- state.routeVerification = verification;
62
- if (!verification.expectedRouteInvocation) {
63
- state.completionStatus = "unchecked";
64
- state.completionReason = null;
65
- return;
66
- }
67
-
68
- state.completionStatus = verification.abnormal ? "abnormal" : "normal";
69
- state.completionReason = verification.reason;
70
- if (verification.abnormal) {
71
- state.lastPreview = "[abnormal] tfx-route.sh evidence missing";
72
- }
73
- }
74
-
75
- function nowMs() {
76
- return Date.now();
77
- }
78
-
79
- const args = parseArgs(process.argv.slice(2));
80
- if (!args.config) {
81
- console.error("사용법: node native-supervisor.mjs --config <path>");
82
- process.exit(1);
83
- }
84
-
85
- const config = await readJson(args.config);
86
- const {
87
- sessionName,
88
- teamName = sessionName,
89
- runtimeFile,
90
- logsDir,
91
- startupDelayMs = 3000,
92
- members = [],
93
- } = config;
94
-
95
- mkdirSync(logsDir, { recursive: true });
96
- mkdirSync(dirname(runtimeFile), { recursive: true });
97
-
98
- const startedAt = nowMs();
99
- const processMap = new Map();
100
-
101
- function memberStateSnapshot() {
102
- const states = [];
103
- for (const m of members) {
104
- const state = processMap.get(m.name);
105
- states.push({
106
- name: m.name,
107
- role: m.role,
108
- cli: m.cli,
109
- agentId: m.agentId,
110
- command: m.command,
111
- pid: state?.child?.pid || null,
112
- status: state?.status || "unknown",
113
- exitCode: state?.exitCode ?? null,
114
- lastPreview: state?.lastPreview || "",
115
- completionStatus: state?.completionStatus || null,
116
- completionReason: state?.completionReason || null,
117
- routeVerification: state?.routeVerification || null,
118
- logFile: state?.logFile || null,
119
- errFile: state?.errFile || null,
120
- workerSandboxRoot: state?.workerSandboxRoot || null,
121
- });
122
- }
123
- return states;
124
- }
125
-
126
- function writeRuntime(controlPort) {
127
- const runtime = {
128
- sessionName,
129
- supervisorPid: process.pid,
130
- controlUrl: `http://127.0.0.1:${controlPort}`,
131
- startedAt,
132
- members: memberStateSnapshot(),
133
- };
134
- writeFileSync(runtimeFile, JSON.stringify(runtime, null, 2) + "\n");
135
- }
136
-
137
- // Shell metacharacters that can be used for command injection.
138
- // member.command is a CLI invocation string (e.g. "codex --flag value").
139
- // shell: true is required on Windows for .cmd executables, so we validate
140
- // the command string instead of removing the shell option.
141
- const SAFE_COMMAND_RE = /^[a-zA-Z0-9 _./:@"'=\-\\]+$/;
142
-
143
- function validateMemberCommand(command, memberName) {
144
- if (typeof command !== "string" || command.trim().length === 0) {
145
- throw new Error(
146
- `member "${memberName}": command must be a non-empty string`,
147
- );
148
- }
149
- if (!SAFE_COMMAND_RE.test(command)) {
150
- throw new Error(
151
- `member "${memberName}": command contains disallowed characters — ` +
152
- `shell metacharacters (;&|$\`()<>{}\\n\\r) are not permitted`,
153
- );
154
- }
155
- }
156
-
157
- function spawnMember(member) {
158
- const command = ensureBashScriptExecution(member.command, {
159
- bashCommand: resolveBashExecutable(),
160
- });
161
- validateMemberCommand(command, member.name);
162
-
163
- const outPath = join(logsDir, `${member.name}.out.log`);
164
- const errPath = join(logsDir, `${member.name}.err.log`);
165
-
166
- const outWs = createWriteStream(outPath, { flags: "a" });
167
- const errWs = createWriteStream(errPath, { flags: "a" });
168
-
169
- const baseEnv = {
170
- ...process.env,
171
- TERM:
172
- process.env.TERM && process.env.TERM !== "dumb"
173
- ? process.env.TERM
174
- : "xterm-256color",
175
- };
176
- // Worker members get HOME-isolated sandboxes, but antigravity-family agents
177
- // (agy/gemini) are carved out inside buildWorkerSandboxEnv so their keyring/
178
- // OAuth auth resolves — pass member.cli so the carve-out applies in-process too.
179
- const sandbox =
180
- member.role === "worker"
181
- ? buildWorkerSandboxEnv({
182
- cwd: config.workdir || process.cwd(),
183
- sessionId: `${sessionName}-${member.name}`,
184
- agent: member.cli,
185
- env: baseEnv,
186
- })
187
- : { env: {}, root: null };
188
-
189
- const child = spawn(command, {
190
- shell: true,
191
- env: { ...baseEnv, ...sandbox.env },
192
- stdio: ["pipe", "pipe", "pipe"],
193
- windowsHide: true,
194
- });
195
-
196
- const state = {
197
- member,
198
- child,
199
- outWs,
200
- errWs,
201
- logFile: outPath,
202
- errFile: errPath,
203
- status: "running",
204
- exitCode: null,
205
- lastPreview: "",
206
- workerSandboxRoot: sandbox.root || null,
207
- };
208
-
209
- child.stdout.on("data", (buf) => {
210
- outWs.write(buf);
211
- const txt = safeText(buf).trim();
212
- if (txt) {
213
- const lines = txt.split(/\r?\n/).filter(Boolean);
214
- if (lines.length)
215
- state.lastPreview = lines[lines.length - 1].slice(0, 280);
216
- }
217
- });
218
-
219
- child.stderr.on("data", (buf) => {
220
- errWs.write(buf);
221
- const txt = safeText(buf).trim();
222
- if (txt) {
223
- const lines = txt.split(/\r?\n/).filter(Boolean);
224
- if (lines.length)
225
- state.lastPreview = `[err] ${lines[lines.length - 1].slice(0, 260)}`;
226
- }
227
- });
228
-
229
- child.on("exit", (code) => {
230
- state.status = "exited";
231
- state.exitCode = code;
232
- finalizeRouteVerification(state);
233
- try {
234
- outWs.end();
235
- } catch {}
236
- try {
237
- errWs.end();
238
- } catch {}
239
- maybeAutoShutdown();
240
- });
241
-
242
- child.on("error", (err) => {
243
- state.status = "exited";
244
- state.exitCode = -1;
245
- state.lastPreview = `[spawn error] ${err.message}`;
246
- try {
247
- outWs.end();
248
- } catch {}
249
- try {
250
- errWs.end();
251
- } catch {}
252
- maybeAutoShutdown();
253
- });
254
-
255
- processMap.set(member.name, state);
256
- }
257
-
258
- function sendInput(memberName, text) {
259
- const state = processMap.get(memberName);
260
- if (!state) return { ok: false, error: "member_not_found" };
261
- if (state.status !== "running")
262
- return { ok: false, error: "member_not_running" };
263
- try {
264
- state.child.stdin.write(`${safeText(text)}\n`);
265
- return { ok: true };
266
- } catch (e) {
267
- return { ok: false, error: e.message };
268
- }
269
- }
270
-
271
- function interruptMember(memberName) {
272
- const state = processMap.get(memberName);
273
- if (!state) return { ok: false, error: "member_not_found" };
274
- if (state.status !== "running")
275
- return { ok: false, error: "member_not_running" };
276
-
277
- let signaled = false;
278
- try {
279
- signaled = state.child.kill("SIGINT");
280
- } catch {
281
- signaled = false;
282
- }
283
-
284
- if (!signaled) {
285
- try {
286
- state.child.stdin.write("\u0003");
287
- signaled = true;
288
- } catch {
289
- signaled = false;
290
- }
291
- }
292
-
293
- return signaled ? { ok: true } : { ok: false, error: "interrupt_failed" };
294
- }
295
-
296
- let isShuttingDown = false;
297
-
298
- function maybeAutoShutdown() {
299
- if (isShuttingDown) return;
300
- const allExited = [...processMap.values()].every(
301
- (s) => s.status === "exited",
302
- );
303
- if (!allExited) return;
304
- shutdown();
305
- }
306
-
307
- async function shutdown() {
308
- if (isShuttingDown) return;
309
- isShuttingDown = true;
310
-
311
- for (const state of processMap.values()) {
312
- if (state.status === "running") {
313
- try {
314
- state.child.stdin.write("exit\n");
315
- } catch {}
316
- try {
317
- state.child.kill("SIGTERM");
318
- } catch {}
319
- }
320
- try {
321
- state.outWs.end();
322
- } catch {}
323
- try {
324
- state.errWs.end();
325
- } catch {}
326
- }
327
-
328
- try {
329
- await forceCleanupTeam(teamName);
330
- } catch {}
331
-
332
- setTimeout(() => {
333
- for (const state of processMap.values()) {
334
- if (state.status === "running") {
335
- const pid = state.child?.pid;
336
- if (process.platform === "win32" && Number.isInteger(pid) && pid > 0) {
337
- // Windows: 프로세스 트리 전체 강제 종료 (손자 MCP 서버 포함)
338
- try {
339
- execSyncSupervisor(`taskkill /T /F /PID ${pid}`, {
340
- stdio: "pipe",
341
- windowsHide: true,
342
- timeout: 5000,
343
- });
344
- } catch {}
345
- } else {
346
- try {
347
- state.child.kill("SIGKILL");
348
- } catch {}
349
- }
350
- }
351
- }
352
- process.exit(0);
353
- }, 1200).unref();
354
- }
355
-
356
- for (const member of members) {
357
- spawnMember(member);
358
- }
359
-
360
- const server = createServer(async (req, res) => {
361
- const send = (code, obj) => {
362
- res.writeHead(code, { "Content-Type": "application/json" });
363
- res.end(JSON.stringify(obj));
364
- };
365
-
366
- if (req.method === "GET" && (req.url === "/" || req.url === "/status")) {
367
- return send(200, {
368
- ok: true,
369
- data: {
370
- sessionName,
371
- supervisorPid: process.pid,
372
- uptimeMs: nowMs() - startedAt,
373
- members: memberStateSnapshot(),
374
- },
375
- });
376
- }
377
-
378
- if (req.method !== "POST") {
379
- return send(405, { ok: false, error: "method_not_allowed" });
380
- }
381
-
382
- let body = {};
383
- try {
384
- const MAX_BODY = 1024 * 1024;
385
- const chunks = [];
386
- let totalLen = 0;
387
- for await (const c of req) {
388
- totalLen += c.length;
389
- if (totalLen > MAX_BODY) {
390
- send(413, { ok: false, error: "payload_too_large" });
391
- return;
392
- }
393
- chunks.push(c);
394
- }
395
- const raw = Buffer.concat(chunks).toString("utf8") || "{}";
396
- body = JSON.parse(raw);
397
- } catch {
398
- return send(400, { ok: false, error: "invalid_json" });
399
- }
400
-
401
- if (req.url === "/send") {
402
- const { member, text } = body;
403
- const r = sendInput(member, text);
404
- return send(r.ok ? 200 : 400, r);
405
- }
406
-
407
- if (req.url === "/interrupt") {
408
- const { member } = body;
409
- const r = interruptMember(member);
410
- return send(r.ok ? 200 : 400, r);
411
- }
412
-
413
- if (req.url === "/control") {
414
- const { member, command = "", reason = "" } = body;
415
- const controlMsg = `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`;
416
- const a = sendInput(member, controlMsg);
417
- if (!a.ok) return send(400, a);
418
- if (String(command).toLowerCase() === "interrupt") {
419
- const b = interruptMember(member);
420
- if (!b.ok) return send(400, b);
421
- }
422
- return send(200, { ok: true });
423
- }
424
-
425
- if (req.url === "/stop") {
426
- send(200, { ok: true });
427
- shutdown();
428
- return;
429
- }
430
-
431
- return send(404, { ok: false, error: "not_found" });
432
- });
433
-
434
- server.on("error", (err) => {
435
- console.error("[native-supervisor] HTTP server error:", err.message);
436
- process.exit(1);
437
- });
438
-
439
- server.listen(0, "127.0.0.1", () => {
440
- const address = server.address();
441
- const port = typeof address === "object" && address ? address.port : null;
442
- if (!port) {
443
- console.error("native supervisor 포트 바인딩 실패");
444
- process.exit(1);
445
- }
446
-
447
- writeRuntime(port);
448
-
449
- // CLI 초기화 후 프롬프트 주입
450
- setTimeout(() => {
451
- for (const m of members) {
452
- if (m.prompt) {
453
- sendInput(m.name, m.prompt);
454
- }
455
- }
456
- }, startupDelayMs).unref();
457
- });
458
-
459
- process.on("SIGINT", shutdown);
460
- process.on("SIGTERM", shutdown);