@triflux/remote 10.1.0 → 10.26.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 (162) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2179 -709
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +78 -17
  9. package/hub/team/build-worker-prompt.mjs +73 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +1130 -0
  12. package/hub/team/claude-native-bridge.mjs +1011 -0
  13. package/hub/team/claude-session-projection.mjs +74 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +151 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +97 -13
  24. package/hub/team/cli/commands/start/start-headless.mjs +155 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/help.mjs +1 -1
  33. package/hub/team/cli/index.mjs +2 -2
  34. package/hub/team/cli/manifest.mjs +38 -8
  35. package/hub/team/cli/render.mjs +52 -30
  36. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  37. package/hub/team/cli/services/hub-client.mjs +44 -16
  38. package/hub/team/cli/services/member-selector.mjs +37 -30
  39. package/hub/team/cli/services/native-control.mjs +144 -116
  40. package/hub/team/cli/services/runtime-mode.mjs +61 -4
  41. package/hub/team/cli/services/state-store.mjs +66 -9
  42. package/hub/team/cli/services/task-model.mjs +40 -30
  43. package/hub/team/codex-review.mjs +521 -0
  44. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  45. package/hub/team/conductor-registry.mjs +121 -0
  46. package/hub/team/conductor.mjs +1379 -671
  47. package/hub/team/consensus-meta.mjs +176 -0
  48. package/hub/team/dashboard-anchor.mjs +4 -5
  49. package/hub/team/dashboard-layout.mjs +3 -1
  50. package/hub/team/dashboard-open.mjs +20 -118
  51. package/hub/team/dashboard.mjs +76 -28
  52. package/hub/team/event-log.mjs +18 -10
  53. package/hub/team/execution-mode.mjs +256 -0
  54. package/hub/team/extract-completion-payload.mjs +110 -0
  55. package/hub/team/git-preflight.mjs +286 -0
  56. package/hub/team/handoff.mjs +49 -18
  57. package/hub/team/headless-bridge-session.mjs +17 -0
  58. package/hub/team/headless.mjs +2213 -1156
  59. package/hub/team/health-probe.mjs +232 -69
  60. package/hub/team/interactive-native-launcher.mjs +106 -0
  61. package/hub/team/interactive-native-worker.mjs +80 -0
  62. package/hub/team/interactive-tui-transport.mjs +465 -0
  63. package/hub/team/launcher-template.mjs +49 -17
  64. package/hub/team/lead-control.mjs +91 -4
  65. package/hub/team/mcp-selector.mjs +145 -0
  66. package/hub/team/native-supervisor.mjs +90 -26
  67. package/hub/team/native.mjs +120 -42
  68. package/hub/team/nativeProxy.mjs +184 -113
  69. package/hub/team/notify.mjs +147 -78
  70. package/hub/team/orchestrator.mjs +173 -161
  71. package/hub/team/pane.mjs +43 -20
  72. package/hub/team/process-cleanup.mjs +69 -25
  73. package/hub/team/psmux.mjs +1022 -281
  74. package/hub/team/recovery-store.mjs +149 -0
  75. package/hub/team/remote-probe.mjs +68 -52
  76. package/hub/team/remote-session.mjs +131 -59
  77. package/hub/team/remote-watcher.mjs +61 -33
  78. package/hub/team/retry-state-machine.mjs +336 -0
  79. package/hub/team/routing.mjs +51 -25
  80. package/hub/team/runtime-strategy.mjs +200 -0
  81. package/hub/team/schemas/worker-completion.json +39 -0
  82. package/hub/team/sentinel-capture.mjs +127 -0
  83. package/hub/team/session-sync.mjs +153 -6
  84. package/hub/team/session.mjs +574 -611
  85. package/hub/team/shared.mjs +13 -13
  86. package/hub/team/staleState.mjs +72 -30
  87. package/hub/team/swarm-cli.mjs +390 -0
  88. package/hub/team/swarm-hypervisor.mjs +1544 -193
  89. package/hub/team/swarm-intent.mjs +216 -0
  90. package/hub/team/swarm-locks.mjs +140 -25
  91. package/hub/team/swarm-planner.mjs +121 -21
  92. package/hub/team/swarm-preflight.mjs +319 -0
  93. package/hub/team/swarm-reconciler.mjs +128 -23
  94. package/hub/team/synapse-cli.mjs +458 -0
  95. package/hub/team/synapse-http.mjs +60 -0
  96. package/hub/team/synapse-registry.mjs +329 -0
  97. package/hub/team/terminal-opener.mjs +216 -0
  98. package/hub/team/tui-core.mjs +292 -0
  99. package/hub/team/tui-lite.mjs +237 -148
  100. package/hub/team/tui-remote-adapter.mjs +18 -34
  101. package/hub/team/tui-synapse.mjs +223 -0
  102. package/hub/team/tui-viewer.mjs +108 -48
  103. package/hub/team/tui-widgets.mjs +262 -0
  104. package/hub/team/tui.mjs +931 -439
  105. package/hub/team/worker-completion-validator.mjs +84 -0
  106. package/hub/team/worker-sandbox.mjs +97 -0
  107. package/hub/team/worker-signal.mjs +263 -0
  108. package/hub/team/worker-signal.types.d.ts +52 -0
  109. package/hub/team/worktree-lifecycle.mjs +517 -63
  110. package/hub/team/wt-manager.mjs +297 -26
  111. package/hub/tools.mjs +477 -252
  112. package/hub/tray.mjs +411 -368
  113. package/hub/workers/antigravity-route-worker.mjs +169 -0
  114. package/hub/workers/claude-worker.mjs +162 -118
  115. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  116. package/hub/workers/codex-mcp.mjs +724 -507
  117. package/hub/workers/delegator-mcp.mjs +570 -499
  118. package/hub/workers/factory.mjs +103 -21
  119. package/hub/workers/gemini-worker.mjs +126 -284
  120. package/hub/workers/interface.mjs +24 -2
  121. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  122. package/hub/workers/worker-utils.mjs +23 -17
  123. package/package.json +4 -3
  124. package/scripts/lib/agent-json.mjs +27 -0
  125. package/scripts/lib/async.mjs +174 -0
  126. package/scripts/lib/claudemd-scanner.mjs +223 -0
  127. package/scripts/lib/cli-agy.mjs +62 -0
  128. package/scripts/lib/cli-claude.mjs +78 -0
  129. package/scripts/lib/cli-codex.mjs +199 -0
  130. package/scripts/lib/cli-gemini.mjs +37 -0
  131. package/scripts/lib/codex-recovery.sh +50 -0
  132. package/scripts/lib/context.mjs +67 -0
  133. package/scripts/lib/cross-review-utils.mjs +59 -0
  134. package/scripts/lib/doctor-env-checks.mjs +121 -0
  135. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  136. package/scripts/lib/env-probe.mjs +269 -0
  137. package/scripts/lib/env.mjs +14 -0
  138. package/scripts/lib/gemini-profiles.mjs +119 -0
  139. package/scripts/lib/handoff.mjs +187 -0
  140. package/scripts/lib/hook-utils.mjs +16 -0
  141. package/scripts/lib/hub.mjs +48 -0
  142. package/scripts/lib/keyword-rules.mjs +195 -0
  143. package/scripts/lib/logger.mjs +105 -0
  144. package/scripts/lib/mcp-filter.mjs +883 -0
  145. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  146. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  147. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  148. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  149. package/scripts/lib/mcp-health.mjs +773 -0
  150. package/scripts/lib/mcp-manifest.mjs +89 -0
  151. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  152. package/scripts/lib/pid.mjs +63 -0
  153. package/scripts/lib/process-utils.mjs +26 -0
  154. package/scripts/lib/psmux-info.mjs +224 -0
  155. package/scripts/lib/quota.mjs +47 -0
  156. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  157. package/scripts/lib/skill-state.mjs +220 -0
  158. package/scripts/lib/skill-template.mjs +375 -0
  159. package/scripts/lib/team.mjs +75 -0
  160. package/scripts/lib/timeout.mjs +66 -0
  161. package/scripts/lib/tmp.mjs +26 -0
  162. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,145 @@
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,10 +1,21 @@
1
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";
2
10
  import { createServer } from "node:http";
3
- import { spawn, execSync as execSyncSupervisor } from "node:child_process";
4
- import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
5
11
  import { dirname, join } from "node:path";
12
+ import {
13
+ ensureBashScriptExecution,
14
+ resolveBashExecutable,
15
+ } from "@triflux/core/hub/lib/bash-path.mjs";
6
16
  import { verifySlimWrapperRouteExecution } from "./native.mjs";
7
17
  import { forceCleanupTeam } from "./nativeProxy.mjs";
18
+ import { buildWorkerSandboxEnv } from "./worker-sandbox.mjs";
8
19
 
9
20
  const ROUTE_LOG_TAIL_BYTES = 65536;
10
21
 
@@ -106,6 +117,7 @@ function memberStateSnapshot() {
106
117
  routeVerification: state?.routeVerification || null,
107
118
  logFile: state?.logFile || null,
108
119
  errFile: state?.errFile || null,
120
+ workerSandboxRoot: state?.workerSandboxRoot || null,
109
121
  });
110
122
  }
111
123
  return states;
@@ -130,18 +142,23 @@ const SAFE_COMMAND_RE = /^[a-zA-Z0-9 _./:@"'=\-\\]+$/;
130
142
 
131
143
  function validateMemberCommand(command, memberName) {
132
144
  if (typeof command !== "string" || command.trim().length === 0) {
133
- throw new Error(`member "${memberName}": command must be a non-empty string`);
145
+ throw new Error(
146
+ `member "${memberName}": command must be a non-empty string`,
147
+ );
134
148
  }
135
149
  if (!SAFE_COMMAND_RE.test(command)) {
136
150
  throw new Error(
137
151
  `member "${memberName}": command contains disallowed characters — ` +
138
- `shell metacharacters (;&|$\`()<>{}\\n\\r) are not permitted`
152
+ `shell metacharacters (;&|$\`()<>{}\\n\\r) are not permitted`,
139
153
  );
140
154
  }
141
155
  }
142
156
 
143
157
  function spawnMember(member) {
144
- validateMemberCommand(member.command, member.name);
158
+ const command = ensureBashScriptExecution(member.command, {
159
+ bashCommand: resolveBashExecutable(),
160
+ });
161
+ validateMemberCommand(command, member.name);
145
162
 
146
163
  const outPath = join(logsDir, `${member.name}.out.log`);
147
164
  const errPath = join(logsDir, `${member.name}.err.log`);
@@ -149,12 +166,25 @@ function spawnMember(member) {
149
166
  const outWs = createWriteStream(outPath, { flags: "a" });
150
167
  const errWs = createWriteStream(errPath, { flags: "a" });
151
168
 
152
- const child = spawn(member.command, {
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
+ const sandbox =
177
+ member.role === "worker"
178
+ ? buildWorkerSandboxEnv({
179
+ cwd: config.workdir || process.cwd(),
180
+ sessionId: `${sessionName}-${member.name}`,
181
+ env: baseEnv,
182
+ })
183
+ : { env: {}, root: null };
184
+
185
+ const child = spawn(command, {
153
186
  shell: true,
154
- env: {
155
- ...process.env,
156
- TERM: process.env.TERM && process.env.TERM !== "dumb" ? process.env.TERM : "xterm-256color",
157
- },
187
+ env: { ...baseEnv, ...sandbox.env },
158
188
  stdio: ["pipe", "pipe", "pipe"],
159
189
  windowsHide: true,
160
190
  });
@@ -169,6 +199,7 @@ function spawnMember(member) {
169
199
  status: "running",
170
200
  exitCode: null,
171
201
  lastPreview: "",
202
+ workerSandboxRoot: sandbox.root || null,
172
203
  };
173
204
 
174
205
  child.stdout.on("data", (buf) => {
@@ -176,7 +207,8 @@ function spawnMember(member) {
176
207
  const txt = safeText(buf).trim();
177
208
  if (txt) {
178
209
  const lines = txt.split(/\r?\n/).filter(Boolean);
179
- if (lines.length) state.lastPreview = lines[lines.length - 1].slice(0, 280);
210
+ if (lines.length)
211
+ state.lastPreview = lines[lines.length - 1].slice(0, 280);
180
212
  }
181
213
  });
182
214
 
@@ -185,7 +217,8 @@ function spawnMember(member) {
185
217
  const txt = safeText(buf).trim();
186
218
  if (txt) {
187
219
  const lines = txt.split(/\r?\n/).filter(Boolean);
188
- if (lines.length) state.lastPreview = `[err] ${lines[lines.length - 1].slice(0, 260)}`;
220
+ if (lines.length)
221
+ state.lastPreview = `[err] ${lines[lines.length - 1].slice(0, 260)}`;
189
222
  }
190
223
  });
191
224
 
@@ -193,8 +226,12 @@ function spawnMember(member) {
193
226
  state.status = "exited";
194
227
  state.exitCode = code;
195
228
  finalizeRouteVerification(state);
196
- try { outWs.end(); } catch {}
197
- try { errWs.end(); } catch {}
229
+ try {
230
+ outWs.end();
231
+ } catch {}
232
+ try {
233
+ errWs.end();
234
+ } catch {}
198
235
  maybeAutoShutdown();
199
236
  });
200
237
 
@@ -202,8 +239,12 @@ function spawnMember(member) {
202
239
  state.status = "exited";
203
240
  state.exitCode = -1;
204
241
  state.lastPreview = `[spawn error] ${err.message}`;
205
- try { outWs.end(); } catch {}
206
- try { errWs.end(); } catch {}
242
+ try {
243
+ outWs.end();
244
+ } catch {}
245
+ try {
246
+ errWs.end();
247
+ } catch {}
207
248
  maybeAutoShutdown();
208
249
  });
209
250
 
@@ -213,7 +254,8 @@ function spawnMember(member) {
213
254
  function sendInput(memberName, text) {
214
255
  const state = processMap.get(memberName);
215
256
  if (!state) return { ok: false, error: "member_not_found" };
216
- if (state.status !== "running") return { ok: false, error: "member_not_running" };
257
+ if (state.status !== "running")
258
+ return { ok: false, error: "member_not_running" };
217
259
  try {
218
260
  state.child.stdin.write(`${safeText(text)}\n`);
219
261
  return { ok: true };
@@ -225,7 +267,8 @@ function sendInput(memberName, text) {
225
267
  function interruptMember(memberName) {
226
268
  const state = processMap.get(memberName);
227
269
  if (!state) return { ok: false, error: "member_not_found" };
228
- if (state.status !== "running") return { ok: false, error: "member_not_running" };
270
+ if (state.status !== "running")
271
+ return { ok: false, error: "member_not_running" };
229
272
 
230
273
  let signaled = false;
231
274
  try {
@@ -250,7 +293,9 @@ let isShuttingDown = false;
250
293
 
251
294
  function maybeAutoShutdown() {
252
295
  if (isShuttingDown) return;
253
- const allExited = [...processMap.values()].every((s) => s.status === "exited");
296
+ const allExited = [...processMap.values()].every(
297
+ (s) => s.status === "exited",
298
+ );
254
299
  if (!allExited) return;
255
300
  shutdown();
256
301
  }
@@ -261,11 +306,19 @@ async function shutdown() {
261
306
 
262
307
  for (const state of processMap.values()) {
263
308
  if (state.status === "running") {
264
- try { state.child.stdin.write("exit\n"); } catch {}
265
- try { state.child.kill("SIGTERM"); } catch {}
309
+ try {
310
+ state.child.stdin.write("exit\n");
311
+ } catch {}
312
+ try {
313
+ state.child.kill("SIGTERM");
314
+ } catch {}
266
315
  }
267
- try { state.outWs.end(); } catch {}
268
- try { state.errWs.end(); } catch {}
316
+ try {
317
+ state.outWs.end();
318
+ } catch {}
319
+ try {
320
+ state.errWs.end();
321
+ } catch {}
269
322
  }
270
323
 
271
324
  try {
@@ -278,9 +331,17 @@ async function shutdown() {
278
331
  const pid = state.child?.pid;
279
332
  if (process.platform === "win32" && Number.isInteger(pid) && pid > 0) {
280
333
  // Windows: 프로세스 트리 전체 강제 종료 (손자 MCP 서버 포함)
281
- try { execSyncSupervisor(`taskkill /T /F /PID ${pid}`, { stdio: "pipe", windowsHide: true, timeout: 5000 }); } catch {}
334
+ try {
335
+ execSyncSupervisor(`taskkill /T /F /PID ${pid}`, {
336
+ stdio: "pipe",
337
+ windowsHide: true,
338
+ timeout: 5000,
339
+ });
340
+ } catch {}
282
341
  } else {
283
- try { state.child.kill("SIGKILL"); } catch {}
342
+ try {
343
+ state.child.kill("SIGKILL");
344
+ } catch {}
284
345
  }
285
346
  }
286
347
  }
@@ -321,7 +382,10 @@ const server = createServer(async (req, res) => {
321
382
  let totalLen = 0;
322
383
  for await (const c of req) {
323
384
  totalLen += c.length;
324
- if (totalLen > MAX_BODY) { send(413, { ok: false, error: "payload_too_large" }); return; }
385
+ if (totalLen > MAX_BODY) {
386
+ send(413, { ok: false, error: "payload_too_large" });
387
+ return;
388
+ }
325
389
  chunks.push(c);
326
390
  }
327
391
  const raw = Buffer.concat(chunks).toString("utf8") || "{}";
@@ -2,11 +2,12 @@
2
2
  // teammate 프롬프트 템플릿 + 팀 설정 빌더
3
3
  //
4
4
  // Claude Code 네이티브 Agent Teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)
5
- // 환경에서 teammate를 Codex/Gemini CLI 래퍼로 구성하는 유틸리티.
5
+ // 환경에서 teammate를 Codex/Antigravity CLI 래퍼로 구성하는 유틸리티.
6
6
  // SKILL.md가 인라인 프롬프트를 사용하므로, 이 모듈은 CLI(tfx multi --native)에서
7
7
  // 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
8
8
 
9
9
  import * as fs from "node:fs/promises";
10
+ import { createRequire } from "node:module";
10
11
  import os from "node:os";
11
12
  import path from "node:path";
12
13
 
@@ -21,9 +22,13 @@ export const SCOUT_ROLE_CONFIG = {
21
22
  readOnly: true,
22
23
  };
23
24
  const ROUTE_LOG_RE = /\[tfx-route\]/i;
24
- const ROUTE_COMMAND_RE = /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
25
+ const ROUTE_COMMAND_RE =
26
+ /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
25
27
  const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
26
28
  const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
29
+ const _require = createRequire(import.meta.url);
30
+ const AGENT_TO_CLI = _require("./agent-map.json");
31
+ const VALID_ROUTE_AGENTS = new Set(Object.keys(AGENT_TO_CLI));
27
32
 
28
33
  function inferWorkerIndex(agentName = "") {
29
34
  const match = /(\d+)(?!.*\d)/.exec(agentName);
@@ -32,13 +37,46 @@ function inferWorkerIndex(agentName = "") {
32
37
  return Number.isInteger(index) && index > 0 ? index : null;
33
38
  }
34
39
 
35
- function buildRouteEnvPrefix(agentName, workerIndex, searchTool) {
36
- const effectiveWorkerIndex = Number.isInteger(workerIndex) && workerIndex > 0
37
- ? workerIndex
38
- : inferWorkerIndex(agentName);
40
+ function normalizeRouteMode(cli) {
41
+ const normalized = String(cli || "").trim();
42
+ if (["gemini", "antigravity", "agy"].includes(normalized)) {
43
+ return "antigravity";
44
+ }
45
+ if (normalized === "codex") return "codex";
46
+ return "";
47
+ }
48
+
49
+ function resolveRouteCli(cli) {
50
+ const normalized = String(cli || "").trim();
51
+ const mapped = AGENT_TO_CLI[normalized] || normalized;
52
+ return mapped === "gemini" ? "antigravity" : mapped;
53
+ }
54
+
55
+ function resolveSlimWrapperRouteAgent(cli, role) {
56
+ const routeRole = String(role || "").trim();
57
+ if (
58
+ routeRole &&
59
+ VALID_ROUTE_AGENTS.has(routeRole) &&
60
+ !["gemini", "antigravity", "agy"].includes(routeRole)
61
+ ) {
62
+ return routeRole;
63
+ }
64
+
65
+ const resolvedCli = resolveRouteCli(cli);
66
+ return VALID_ROUTE_AGENTS.has(resolvedCli) ? resolvedCli : "codex";
67
+ }
68
+
69
+ function buildRouteEnvPrefix(agentName, workerIndex, searchTool, cli) {
70
+ const effectiveWorkerIndex =
71
+ Number.isInteger(workerIndex) && workerIndex > 0
72
+ ? workerIndex
73
+ : inferWorkerIndex(agentName);
74
+ const routeMode = normalizeRouteMode(cli);
39
75
 
40
76
  let envPrefix = "";
41
- if (effectiveWorkerIndex) envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
77
+ if (routeMode) envPrefix += ` TFX_CLI_MODE="${routeMode}"`;
78
+ if (effectiveWorkerIndex)
79
+ envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
42
80
  if (searchTool) envPrefix += ` TFX_SEARCH_TOOL="${searchTool}"`;
43
81
  return envPrefix;
44
82
  }
@@ -91,7 +129,8 @@ export function verifySlimWrapperRouteExecution(input = {}) {
91
129
  const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
92
130
  const usedRoute = sawRouteCommand || sawRouteLog;
93
131
  const expectedRouteInvocation = promptMentionsRoute;
94
- const abnormal = expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
132
+ const abnormal =
133
+ expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
95
134
  const reason = !abnormal
96
135
  ? null
97
136
  : sawDirectToolBypass
@@ -123,14 +162,26 @@ function getRouteTimeout(role, _mcpProfile) {
123
162
  // tfx-route.sh route_agent()의 DEFAULT_TIMEOUT 기반, 최소 1080초(18분) 보장.
124
163
  // Bash timeout = 이 값 + 60초 여유. 짧은 역할도 네트워크/스케줄 지연 대비.
125
164
  const TIMEOUTS = {
126
- 'build-fixer': 1080, debugger: 1080, executor: 1080,
127
- 'deep-executor': 3600, architect: 3600, planner: 3600,
128
- critic: 3600, analyst: 3600, scientist: 1800,
129
- 'scientist-deep': 3600, 'document-specialist': 1800,
130
- 'code-reviewer': 1800, 'security-reviewer': 1800,
131
- 'quality-reviewer': 1800, verifier: 1800,
132
- designer: 1080, writer: 1080,
133
- explore: 1080, 'test-engineer': 1080, 'qa-tester': 1080,
165
+ "build-fixer": 1080,
166
+ debugger: 1080,
167
+ executor: 1080,
168
+ "deep-executor": 3600,
169
+ architect: 3600,
170
+ planner: 3600,
171
+ critic: 3600,
172
+ analyst: 3600,
173
+ scientist: 1800,
174
+ "scientist-deep": 3600,
175
+ "document-specialist": 1800,
176
+ "code-reviewer": 1800,
177
+ "security-reviewer": 1800,
178
+ "quality-reviewer": 1800,
179
+ verifier: 1800,
180
+ designer: 1080,
181
+ writer: 1080,
182
+ explore: 1080,
183
+ "test-engineer": 1080,
184
+ "qa-tester": 1080,
134
185
  spark: 600,
135
186
  };
136
187
  return TIMEOUTS[role] || 1080;
@@ -172,20 +223,27 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
172
223
  maxIterations = 3,
173
224
  } = opts;
174
225
 
175
- const routeTimeoutSec = getRouteTimeout(role, mcp_profile);
226
+ const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
227
+ const routeTimeoutSec = getRouteTimeout(routeAgent, mcp_profile);
176
228
  const escaped = subtask.replace(/'/g, "'\\''");
177
229
  const pipelineHint = pipelinePhase
178
230
  ? `\n파이프라인 단계: ${pipelinePhase}`
179
- : '';
180
- const routeEnvPrefix = buildRouteEnvPrefix(agentName, workerIndex, searchTool);
181
- const scoutConstraint = (role === "scout" || role === "scientist")
182
- ? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
183
231
  : "";
232
+ const routeEnvPrefix = buildRouteEnvPrefix(
233
+ agentName,
234
+ workerIndex,
235
+ searchTool,
236
+ cli,
237
+ );
238
+ const scoutConstraint =
239
+ role === "scout" || role === "scientist"
240
+ ? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
241
+ : "";
184
242
 
185
243
  // Bash 도구 timeout (모두 600초 이내)
186
- const launchTimeoutMs = 15000; // Step 1: fork + job_id 반환
187
- const waitTimeoutMs = 570000; // Step 2: 내부 폴링 (540초 대기 + 여유)
188
- const resultTimeoutMs = 30000; // Step 3: 결과 읽기
244
+ const launchTimeoutMs = 15000; // Step 1: fork + job_id 반환
245
+ const waitTimeoutMs = 570000; // Step 2: 내부 폴링 (540초 대기 + 여유)
246
+ const resultTimeoutMs = 30000; // Step 3: 결과 읽기
189
247
 
190
248
  return `실행 프로토콜 (subagent_type="${SLIM_WRAPPER_SUBAGENT_TYPE}", async + feedback):
191
249
  MAX_ITERATIONS = ${maxIterations}
@@ -197,14 +255,14 @@ SendMessage(type: "message", recipient: "${leadName}", content: "작업 시작:
197
255
 
198
256
  [HARD CONSTRAINT] 허용 도구: Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용한다.
199
257
  Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
200
- 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Gemini에 위임하라.
258
+ 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
201
259
  이 규칙을 위반하면 작업 실패로 간주한다.${scoutConstraint}
202
260
 
203
261
  gemini/codex를 직접 호출하지 마라. 반드시 tfx-route.sh를 거쳐야 한다.
204
262
  프롬프트를 파일로 저장하지 마라. tfx-route.sh가 인자로 받는다.
205
263
 
206
264
  Step 1 — Async 시작 (즉시 리턴, <1초):
207
- Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} --async "${role}" '"'"'${escaped}'"'"' ${mcp_profile} ${routeTimeoutSec}', timeout: ${launchTimeoutMs})
265
+ Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} --async "${routeAgent}" '"'"'${escaped}'"'"' ${mcp_profile} ${routeTimeoutSec}', timeout: ${launchTimeoutMs})
208
266
  → 출력 한 줄이 JOB_ID이다. 반드시 기억하라.
209
267
 
210
268
  Step 2 — 완료 대기 (내부 폴링, 최대 540초):
@@ -252,9 +310,7 @@ SendMessage(type: "message", recipient: "${leadName}", content: "최종 완료:
252
310
  */
253
311
  export function buildScoutDispatchPrompt(opts = {}) {
254
312
  const { question, scope = "", teamName, taskId, agentName, leadName } = opts;
255
- const subtask = scope
256
- ? `${question} 탐색 범위: ${scope}`
257
- : question;
313
+ const subtask = scope ? `${question} 탐색 범위: ${scope}` : question;
258
314
  return buildSlimWrapperPrompt("codex", {
259
315
  subtask,
260
316
  role: "scientist",
@@ -305,12 +361,20 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
305
361
  } = opts;
306
362
 
307
363
  const escaped = subtask.replace(/'/g, "'\\''");
308
- const pipelineHint = pipelinePhase ? `\n파이프라인 단계: ${pipelinePhase}` : "";
364
+ const pipelineHint = pipelinePhase
365
+ ? `\n파이프라인 단계: ${pipelinePhase}`
366
+ : "";
309
367
  const taskIdRef = taskId ? `taskId: "${taskId}"` : "";
310
368
  const taskIdArg = taskIdRef ? `${taskIdRef}, ` : "";
311
- const routeEnvPrefix = buildRouteEnvPrefix(agentName, workerIndex, searchTool);
369
+ const routeEnvPrefix = buildRouteEnvPrefix(
370
+ agentName,
371
+ workerIndex,
372
+ searchTool,
373
+ cli,
374
+ );
375
+ const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
312
376
 
313
- const routeCmd = `TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} "${role}" '${escaped}' ${mcp_profile}`;
377
+ const routeCmd = `TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} "${routeAgent}" '${escaped}' ${mcp_profile}`;
314
378
 
315
379
  return `하이브리드 psmux 워커 프로토콜:
316
380
 
@@ -335,7 +399,7 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
335
399
  ${pipelineHint}
336
400
  [HARD CONSTRAINT] 너는 Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용할 수 있다.
337
401
  Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
338
- 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Gemini에 위임하라.
402
+ 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
339
403
  이 규칙을 위반하면 작업 실패로 간주한다.
340
404
 
341
405
  gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통해 실행한다.
@@ -374,7 +438,12 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
374
438
  return { completed: [], pending: normalizedTaskIds };
375
439
  }
376
440
 
377
- const resultDir = path.join(os.homedir(), ".claude", "tfx-results", normalizedTeamName);
441
+ const resultDir = path.join(
442
+ os.homedir(),
443
+ ".claude",
444
+ "tfx-results",
445
+ normalizedTeamName,
446
+ );
378
447
 
379
448
  let entries;
380
449
  try {
@@ -387,9 +456,7 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
387
456
  }
388
457
 
389
458
  const availableFiles = new Set(
390
- entries
391
- .filter((entry) => entry.isFile())
392
- .map((entry) => entry.name),
459
+ entries.filter((entry) => entry.isFile()).map((entry) => entry.name),
393
460
  );
394
461
 
395
462
  const completedCandidates = await Promise.all(
@@ -418,7 +485,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
418
485
 
419
486
  const completed = completedCandidates.filter(Boolean);
420
487
  const completedTaskIds = new Set(completed.map((item) => item.taskId));
421
- const pending = normalizedTaskIds.filter((taskId) => !completedTaskIds.has(taskId));
488
+ const pending = normalizedTaskIds.filter(
489
+ (taskId) => !completedTaskIds.has(taskId),
490
+ );
422
491
 
423
492
  return { completed, pending };
424
493
  }
@@ -430,7 +499,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
430
499
  * @returns {string}
431
500
  */
432
501
  export function formatPollReport(pollResult = {}) {
433
- const completed = Array.isArray(pollResult.completed) ? pollResult.completed : [];
502
+ const completed = Array.isArray(pollResult.completed)
503
+ ? pollResult.completed
504
+ : [];
434
505
  const pending = Array.isArray(pollResult.pending) ? pollResult.pending : [];
435
506
  const total = completed.length + pending.length;
436
507
 
@@ -453,7 +524,10 @@ export function formatPollReport(pollResult = {}) {
453
524
  * @returns {string}
454
525
  */
455
526
  function normalizeText(s) {
456
- return String(s || "").toLowerCase().replace(/\s+/g, " ").trim();
527
+ return String(s || "")
528
+ .toLowerCase()
529
+ .replace(/\s+/g, " ")
530
+ .trim();
457
531
  }
458
532
 
459
533
  /**
@@ -523,7 +597,8 @@ export function compressScoutReport(rawReport) {
523
597
  const text = String(rawReport || "");
524
598
 
525
599
  // 파일:라인 패턴 추출 (path/to/file.ext:123 형태 + 뒤따르는 설명)
526
- const fileLineRe = /([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,10}):(\d+)\s*[:\-–—]?\s*(.+)/g;
600
+ const fileLineRe =
601
+ /([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,10}):(\d+)\s*[:\-–—]?\s*(.+)/g;
527
602
  const findings = [];
528
603
  let match;
529
604
  while ((match = fileLineRe.exec(text)) !== null) {
@@ -546,7 +621,10 @@ export function compressScoutReport(rawReport) {
546
621
 
547
622
  // findings를 먼저 계산하고, 남은 공간에 맞춰 summary를 자른다
548
623
  const findingsJson = JSON.stringify(findings);
549
- const findingsBudget = Math.min(findingsJson.length, Math.floor(MAX_CHARS * 0.3));
624
+ const findingsBudget = Math.min(
625
+ findingsJson.length,
626
+ Math.floor(MAX_CHARS * 0.3),
627
+ );
550
628
 
551
629
  let trimmedFindings = findings;
552
630
  if (findingsJson.length > findingsBudget && findings.length > 0) {