@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.
Files changed (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +68 -7
  3. package/hub/server.mjs +2181 -701
  4. package/hub/store-adapter.mjs +101 -37
  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 +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -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 +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +127 -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/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +45 -17
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +21 -119
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1067 -264
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +69 -53
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +62 -34
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1526 -198
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +21 -37
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -45
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +936 -449
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +518 -64
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +663 -337
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. 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") || "{}";
@@ -21,7 +21,8 @@ export const SCOUT_ROLE_CONFIG = {
21
21
  readOnly: true,
22
22
  };
23
23
  const ROUTE_LOG_RE = /\[tfx-route\]/i;
24
- const ROUTE_COMMAND_RE = /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
24
+ const ROUTE_COMMAND_RE =
25
+ /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
25
26
  const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
26
27
  const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
27
28
 
@@ -33,12 +34,14 @@ function inferWorkerIndex(agentName = "") {
33
34
  }
34
35
 
35
36
  function buildRouteEnvPrefix(agentName, workerIndex, searchTool) {
36
- const effectiveWorkerIndex = Number.isInteger(workerIndex) && workerIndex > 0
37
- ? workerIndex
38
- : inferWorkerIndex(agentName);
37
+ const effectiveWorkerIndex =
38
+ Number.isInteger(workerIndex) && workerIndex > 0
39
+ ? workerIndex
40
+ : inferWorkerIndex(agentName);
39
41
 
40
42
  let envPrefix = "";
41
- if (effectiveWorkerIndex) envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
43
+ if (effectiveWorkerIndex)
44
+ envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
42
45
  if (searchTool) envPrefix += ` TFX_SEARCH_TOOL="${searchTool}"`;
43
46
  return envPrefix;
44
47
  }
@@ -91,7 +94,8 @@ export function verifySlimWrapperRouteExecution(input = {}) {
91
94
  const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
92
95
  const usedRoute = sawRouteCommand || sawRouteLog;
93
96
  const expectedRouteInvocation = promptMentionsRoute;
94
- const abnormal = expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
97
+ const abnormal =
98
+ expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
95
99
  const reason = !abnormal
96
100
  ? null
97
101
  : sawDirectToolBypass
@@ -123,14 +127,26 @@ function getRouteTimeout(role, _mcpProfile) {
123
127
  // tfx-route.sh route_agent()의 DEFAULT_TIMEOUT 기반, 최소 1080초(18분) 보장.
124
128
  // Bash timeout = 이 값 + 60초 여유. 짧은 역할도 네트워크/스케줄 지연 대비.
125
129
  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,
130
+ "build-fixer": 1080,
131
+ debugger: 1080,
132
+ executor: 1080,
133
+ "deep-executor": 3600,
134
+ architect: 3600,
135
+ planner: 3600,
136
+ critic: 3600,
137
+ analyst: 3600,
138
+ scientist: 1800,
139
+ "scientist-deep": 3600,
140
+ "document-specialist": 1800,
141
+ "code-reviewer": 1800,
142
+ "security-reviewer": 1800,
143
+ "quality-reviewer": 1800,
144
+ verifier: 1800,
145
+ designer: 1080,
146
+ writer: 1080,
147
+ explore: 1080,
148
+ "test-engineer": 1080,
149
+ "qa-tester": 1080,
134
150
  spark: 600,
135
151
  };
136
152
  return TIMEOUTS[role] || 1080;
@@ -176,16 +192,21 @@ export function buildSlimWrapperPrompt(cli, opts = {}) {
176
192
  const escaped = subtask.replace(/'/g, "'\\''");
177
193
  const pipelineHint = pipelinePhase
178
194
  ? `\n파이프라인 단계: ${pipelinePhase}`
179
- : '';
180
- const routeEnvPrefix = buildRouteEnvPrefix(agentName, workerIndex, searchTool);
181
- const scoutConstraint = (role === "scout" || role === "scientist")
182
- ? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
183
195
  : "";
196
+ const routeEnvPrefix = buildRouteEnvPrefix(
197
+ agentName,
198
+ workerIndex,
199
+ searchTool,
200
+ );
201
+ const scoutConstraint =
202
+ role === "scout" || role === "scientist"
203
+ ? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
204
+ : "";
184
205
 
185
206
  // 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: 결과 읽기
207
+ const launchTimeoutMs = 15000; // Step 1: fork + job_id 반환
208
+ const waitTimeoutMs = 570000; // Step 2: 내부 폴링 (540초 대기 + 여유)
209
+ const resultTimeoutMs = 30000; // Step 3: 결과 읽기
189
210
 
190
211
  return `실행 프로토콜 (subagent_type="${SLIM_WRAPPER_SUBAGENT_TYPE}", async + feedback):
191
212
  MAX_ITERATIONS = ${maxIterations}
@@ -252,9 +273,7 @@ SendMessage(type: "message", recipient: "${leadName}", content: "최종 완료:
252
273
  */
253
274
  export function buildScoutDispatchPrompt(opts = {}) {
254
275
  const { question, scope = "", teamName, taskId, agentName, leadName } = opts;
255
- const subtask = scope
256
- ? `${question} 탐색 범위: ${scope}`
257
- : question;
276
+ const subtask = scope ? `${question} 탐색 범위: ${scope}` : question;
258
277
  return buildSlimWrapperPrompt("codex", {
259
278
  subtask,
260
279
  role: "scientist",
@@ -305,10 +324,16 @@ export function buildHybridWrapperPrompt(cli, opts = {}) {
305
324
  } = opts;
306
325
 
307
326
  const escaped = subtask.replace(/'/g, "'\\''");
308
- const pipelineHint = pipelinePhase ? `\n파이프라인 단계: ${pipelinePhase}` : "";
327
+ const pipelineHint = pipelinePhase
328
+ ? `\n파이프라인 단계: ${pipelinePhase}`
329
+ : "";
309
330
  const taskIdRef = taskId ? `taskId: "${taskId}"` : "";
310
331
  const taskIdArg = taskIdRef ? `${taskIdRef}, ` : "";
311
- const routeEnvPrefix = buildRouteEnvPrefix(agentName, workerIndex, searchTool);
332
+ const routeEnvPrefix = buildRouteEnvPrefix(
333
+ agentName,
334
+ workerIndex,
335
+ searchTool,
336
+ );
312
337
 
313
338
  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}`;
314
339
 
@@ -374,7 +399,12 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
374
399
  return { completed: [], pending: normalizedTaskIds };
375
400
  }
376
401
 
377
- const resultDir = path.join(os.homedir(), ".claude", "tfx-results", normalizedTeamName);
402
+ const resultDir = path.join(
403
+ os.homedir(),
404
+ ".claude",
405
+ "tfx-results",
406
+ normalizedTeamName,
407
+ );
378
408
 
379
409
  let entries;
380
410
  try {
@@ -387,9 +417,7 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
387
417
  }
388
418
 
389
419
  const availableFiles = new Set(
390
- entries
391
- .filter((entry) => entry.isFile())
392
- .map((entry) => entry.name),
420
+ entries.filter((entry) => entry.isFile()).map((entry) => entry.name),
393
421
  );
394
422
 
395
423
  const completedCandidates = await Promise.all(
@@ -418,7 +446,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
418
446
 
419
447
  const completed = completedCandidates.filter(Boolean);
420
448
  const completedTaskIds = new Set(completed.map((item) => item.taskId));
421
- const pending = normalizedTaskIds.filter((taskId) => !completedTaskIds.has(taskId));
449
+ const pending = normalizedTaskIds.filter(
450
+ (taskId) => !completedTaskIds.has(taskId),
451
+ );
422
452
 
423
453
  return { completed, pending };
424
454
  }
@@ -430,7 +460,9 @@ export async function pollTeamResults(teamName, expectedTaskIds = []) {
430
460
  * @returns {string}
431
461
  */
432
462
  export function formatPollReport(pollResult = {}) {
433
- const completed = Array.isArray(pollResult.completed) ? pollResult.completed : [];
463
+ const completed = Array.isArray(pollResult.completed)
464
+ ? pollResult.completed
465
+ : [];
434
466
  const pending = Array.isArray(pollResult.pending) ? pollResult.pending : [];
435
467
  const total = completed.length + pending.length;
436
468
 
@@ -453,7 +485,10 @@ export function formatPollReport(pollResult = {}) {
453
485
  * @returns {string}
454
486
  */
455
487
  function normalizeText(s) {
456
- return String(s || "").toLowerCase().replace(/\s+/g, " ").trim();
488
+ return String(s || "")
489
+ .toLowerCase()
490
+ .replace(/\s+/g, " ")
491
+ .trim();
457
492
  }
458
493
 
459
494
  /**
@@ -523,7 +558,8 @@ export function compressScoutReport(rawReport) {
523
558
  const text = String(rawReport || "");
524
559
 
525
560
  // 파일:라인 패턴 추출 (path/to/file.ext:123 형태 + 뒤따르는 설명)
526
- const fileLineRe = /([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,10}):(\d+)\s*[:\-–—]?\s*(.+)/g;
561
+ const fileLineRe =
562
+ /([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,10}):(\d+)\s*[:\-–—]?\s*(.+)/g;
527
563
  const findings = [];
528
564
  let match;
529
565
  while ((match = fileLineRe.exec(text)) !== null) {
@@ -546,7 +582,10 @@ export function compressScoutReport(rawReport) {
546
582
 
547
583
  // findings를 먼저 계산하고, 남은 공간에 맞춰 summary를 자른다
548
584
  const findingsJson = JSON.stringify(findings);
549
- const findingsBudget = Math.min(findingsJson.length, Math.floor(MAX_CHARS * 0.3));
585
+ const findingsBudget = Math.min(
586
+ findingsJson.length,
587
+ Math.floor(MAX_CHARS * 0.3),
588
+ );
550
589
 
551
590
  let trimmedFindings = findings;
552
591
  if (findingsJson.length > findingsBudget && findings.length > 0) {