@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
@@ -1,21 +1,100 @@
1
- // hub/workers/factory.mjs — Worker 생성 팩토리
2
-
3
- import { GeminiWorker } from './gemini-worker.mjs';
4
- import { ClaudeWorker } from './claude-worker.mjs';
5
- import { CodexMcpWorker } from './codex-mcp.mjs';
6
- import { DelegatorMcpWorker } from './delegator-mcp.mjs';
7
-
8
- export function createWorker(type, opts = {}) {
9
- switch (type) {
10
- case 'gemini':
11
- return new GeminiWorker(opts);
12
- case 'claude':
13
- return new ClaudeWorker(opts);
14
- case 'codex':
15
- return new CodexMcpWorker(opts);
16
- case 'delegator':
17
- return new DelegatorMcpWorker(opts);
18
- default:
19
- throw new Error(`Unknown worker type: ${type}`);
20
- }
21
- }
1
+ // hub/workers/factory.mjs — Worker 생성 팩토리
2
+ //
3
+ // Supported worker types:
4
+ // - 'gemini' → GeminiWorker
5
+ // - 'claude' → ClaudeWorker
6
+ // - 'codex' → CodexMcpWorker (default) or CodexAppServerWorker when
7
+ // opts.transport === 'app-server'
8
+ // - 'codex-app-server' → CodexAppServerWorker (explicit alias)
9
+ // - 'delegator' → DelegatorMcpWorker
10
+ //
11
+ // For the codex app-server transport the factory injects a default
12
+ // `publishCallback` that forwards each envelope to `/bridge/publish` via the
13
+ // existing `requestJson` helper. Callers can override by passing their own
14
+ // `publishCallback` or swap the transport with `requestJsonFn`.
15
+
16
+ import { ClaudeWorker } from "./claude-worker.mjs";
17
+ import { CodexAppServerWorker } from "./codex-app-server-worker.mjs";
18
+ import { GeminiWorker } from "./gemini-worker.mjs";
19
+
20
+ /**
21
+ * Build a best-effort publishCallback that posts the envelope to
22
+ * `/bridge/publish`. Failures are swallowed so publish pressure never crashes
23
+ * a running worker turn.
24
+ * @param {(path: string, opts?: object) => Promise<unknown>} [requestJsonFn]
25
+ * @returns {(publishMessage: object) => Promise<void>}
26
+ */
27
+ function defaultPublishCallback(requestJsonFn = null) {
28
+ return async (publishMessage) => {
29
+ try {
30
+ const publishRequestJson =
31
+ requestJsonFn || (await import("@triflux/core/hub/bridge.mjs")).requestJson;
32
+ await publishRequestJson("/bridge/publish", { body: publishMessage });
33
+ } catch {
34
+ // best-effort; publish failures must not crash the worker
35
+ }
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Construct a CodexAppServerWorker with a default publishCallback wired to
41
+ * `requestJson('/bridge/publish', ...)` unless the caller supplied one.
42
+ *
43
+ * Issue #95 P1 #4 validation: the app-server transport does not yet implement
44
+ * the server-initiated approval / `tool/requestUserInput` round-trip. Any
45
+ * `approvalPolicy !== 'never'` would cause a codex turn to hang waiting for
46
+ * an approval response the worker cannot produce. Reject such configs at
47
+ * factory time with a clear, actionable message.
48
+ *
49
+ * @param {object} [opts]
50
+ */
51
+ async function createCodexWorker(opts = {}) {
52
+ const { transport, requestJsonFn, publishCallback, ...rest } = opts;
53
+
54
+ if (transport === "app-server") {
55
+ const policy = rest.approvalPolicy;
56
+ if (policy !== undefined && policy !== null && policy !== "never") {
57
+ throw new Error(
58
+ `codex app-server transport currently requires approvalPolicy='never' (got '${policy}'). ` +
59
+ "The server-initiated approval / tool/requestUserInput round-trip is not yet implemented. " +
60
+ "Set approvalPolicy='never' or use transport='mcp'. " +
61
+ "Tracked in follow-up issue.",
62
+ );
63
+ }
64
+ return new CodexAppServerWorker({
65
+ ...rest,
66
+ publishCallback:
67
+ typeof publishCallback === "function"
68
+ ? publishCallback
69
+ : defaultPublishCallback(requestJsonFn),
70
+ });
71
+ }
72
+
73
+ // Default (and transport === 'mcp'): CodexMcpWorker with zero new deps.
74
+ const { CodexMcpWorker } = await import("./codex-mcp.mjs");
75
+ return new CodexMcpWorker(rest);
76
+ }
77
+
78
+ /**
79
+ * @param {'gemini'|'claude'|'codex'|'codex-app-server'|'delegator'} type
80
+ * @param {object} [opts]
81
+ * @returns {Promise<import('./interface.mjs').IWorker>}
82
+ */
83
+ export async function createWorker(type, opts = {}) {
84
+ switch (type) {
85
+ case "gemini":
86
+ return new GeminiWorker(opts);
87
+ case "claude":
88
+ return new ClaudeWorker(opts);
89
+ case "codex":
90
+ return createCodexWorker(opts);
91
+ case "codex-app-server":
92
+ return createCodexWorker({ ...opts, transport: "app-server" });
93
+ case "delegator": {
94
+ const { DelegatorMcpWorker } = await import("./delegator-mcp.mjs");
95
+ return new DelegatorMcpWorker(opts);
96
+ }
97
+ default:
98
+ throw new Error(`Unknown worker type: ${type}`);
99
+ }
100
+ }
@@ -1,20 +1,68 @@
1
1
  // hub/workers/gemini-worker.mjs — Gemini headless subprocess 래퍼
2
2
  // ADR-006: --output-format stream-json 기반 단발 실행 워커.
3
3
 
4
- import { spawn } from 'node:child_process';
5
- import { isAbsolute } from 'node:path';
6
- import readline from 'node:readline';
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync } from "node:fs";
6
+ import { extname } from "node:path";
7
+ import readline from "node:readline";
7
8
 
8
- import { extractText, terminateChild, withRetry } from './worker-utils.mjs';
9
+ import { extractText, terminateChild, withRetry } from "./worker-utils.mjs";
9
10
 
10
11
  const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
11
12
  const DEFAULT_KILL_GRACE_MS = 1000;
12
13
 
13
14
  function toStringList(value) {
14
15
  if (!Array.isArray(value)) return [];
15
- return value
16
- .map((item) => String(item ?? '').trim())
17
- .filter(Boolean);
16
+ return value.map((item) => String(item ?? "").trim()).filter(Boolean);
17
+ }
18
+
19
+ /**
20
+ * Windows에서 cmd.exe에 전달할 인자를 안전하게 quoting한다.
21
+ * 빈 문자열은 ""로, 특수문자 포함 시 큰따옴표로 감싼다.
22
+ */
23
+ function quoteWindowsCmdArg(value) {
24
+ const raw = String(value ?? "").replace(/[\r\n]/g, " ");
25
+ if (raw.length === 0) return '""';
26
+ if (!/[\s"()^&|<>%!]/.test(raw)) return raw;
27
+ const escaped = raw
28
+ .replace(/%/g, "%%")
29
+ .replace(/(\\*)"/g, '$1$1\\"')
30
+ .replace(/(\\+)$/g, "$1$1");
31
+ return `"${escaped}"`;
32
+ }
33
+
34
+ /**
35
+ * Windows npm shim(.cmd) spawn 문제를 해결한다.
36
+ * - command -v가 확장자 없는 경로를 반환하면 .exe → .cmd → .bat 순으로 탐색
37
+ * - .cmd/.bat는 CVE-2024-27980 이후 shell:false에서 실행 불가하므로 cmd.exe /d /s /c 경유
38
+ * - .exe는 직접 실행
39
+ * - non-Windows는 그대로 통과
40
+ */
41
+ function buildSpawnSpec(command, args) {
42
+ if (process.platform !== "win32") {
43
+ return { command, args, shell: false };
44
+ }
45
+
46
+ let resolved = command;
47
+ if (!extname(resolved)) {
48
+ for (const ext of [".exe", ".cmd", ".bat"]) {
49
+ if (existsSync(resolved + ext)) {
50
+ resolved = resolved + ext;
51
+ break;
52
+ }
53
+ }
54
+ }
55
+
56
+ if (/\.(cmd|bat)$/i.test(resolved)) {
57
+ const line = [resolved, ...args].map(quoteWindowsCmdArg).join(" ");
58
+ return {
59
+ command: "cmd.exe",
60
+ args: ["/d", "/s", "/v:off", "/c", line],
61
+ shell: false,
62
+ };
63
+ }
64
+
65
+ return { command: resolved, args, shell: false };
18
66
  }
19
67
 
20
68
  function safeJsonParse(line) {
@@ -36,25 +84,25 @@ function buildGeminiArgs(options) {
36
84
  const args = [];
37
85
 
38
86
  if (options.model) {
39
- args.push('--model', options.model);
87
+ args.push("--model", options.model);
40
88
  }
41
89
 
42
90
  if (options.approvalMode) {
43
- args.push('--approval-mode', options.approvalMode);
91
+ args.push("--approval-mode", options.approvalMode);
44
92
  } else if (options.yolo !== false) {
45
- args.push('--yolo');
93
+ args.push("--yolo");
46
94
  }
47
95
 
48
96
  const allowedMcpServers = toStringList(options.allowedMcpServerNames);
49
97
  if (allowedMcpServers.length) {
50
- args.push('--allowed-mcp-server-names', ...allowedMcpServers);
98
+ args.push("--allowed-mcp-server-names", ...allowedMcpServers);
51
99
  }
52
100
 
53
101
  const extraArgs = toStringList(options.extraArgs);
54
102
  if (extraArgs.length) args.push(...extraArgs);
55
103
 
56
- args.push('--prompt', options.promptArgument ?? '');
57
- args.push('--output-format', 'stream-json');
104
+ args.push("--prompt", options.promptArgument ?? "");
105
+ args.push("--output-format", "stream-json");
58
106
 
59
107
  return args;
60
108
  }
@@ -66,49 +114,59 @@ function createWorkerError(message, details = {}) {
66
114
  }
67
115
 
68
116
  function normalizeRetryOptions(retryOptions) {
69
- if (!retryOptions || typeof retryOptions !== 'object') {
117
+ if (!retryOptions || typeof retryOptions !== "object") {
70
118
  return Object.freeze({});
71
119
  }
72
120
  return Object.freeze({ ...retryOptions });
73
121
  }
74
122
 
75
123
  function isGeminiRetryable(error) {
76
- return error?.code === 'WORKER_EXIT'
77
- && error?.result?.exitCode !== 0
78
- && error?.result?.exitCode !== 2;
124
+ return (
125
+ error?.code === "WORKER_EXIT" &&
126
+ error?.result?.exitCode !== 0 &&
127
+ error?.result?.exitCode !== 2
128
+ );
79
129
  }
80
130
 
81
131
  function detectGeminiCategory(error) {
82
- const combined = `${error?.message || ''}\n${error?.stderr || ''}`.toLowerCase();
83
-
84
- if (/(unauthorized|forbidden|auth|login|token|credential|apikey|api key)/.test(combined)) {
85
- return 'auth';
132
+ const combined =
133
+ `${error?.message || ""}\n${error?.stderr || ""}`.toLowerCase();
134
+
135
+ if (
136
+ /(unauthorized|forbidden|auth|login|token|credential|apikey|api key)/.test(
137
+ combined,
138
+ )
139
+ ) {
140
+ return "auth";
86
141
  }
87
- if (error?.result?.exitCode === 2 || /expected stream-json|unknown option|invalid option|config/.test(combined)) {
88
- return 'config';
142
+ if (
143
+ error?.result?.exitCode === 2 ||
144
+ /expected stream-json|unknown option|invalid option|config/.test(combined)
145
+ ) {
146
+ return "config";
89
147
  }
90
- if (error?.code === 'WORKER_EVENT_ERROR') {
91
- return 'input';
148
+ if (error?.code === "WORKER_EVENT_ERROR") {
149
+ return "input";
92
150
  }
93
151
 
94
- return 'transient';
152
+ return "transient";
95
153
  }
96
154
 
97
155
  function buildGeminiErrorInfo(error, attempts) {
98
156
  const category = detectGeminiCategory(error);
99
157
  const retryable = isGeminiRetryable(error);
100
- let recovery = 'Retry the Gemini worker after correcting the reported issue.';
101
-
102
- if (category === 'auth') {
103
- recovery = 'Refresh the Gemini authentication state and retry.';
104
- } else if (category === 'config') {
105
- recovery = 'Check the Gemini CLI flags and worker configuration.';
106
- } else if (category === 'input') {
107
- recovery = 'Check the Gemini request payload and streamed event format.';
158
+ let recovery = "Retry the Gemini worker after correcting the reported issue.";
159
+
160
+ if (category === "auth") {
161
+ recovery = "Refresh the Gemini authentication state and retry.";
162
+ } else if (category === "config") {
163
+ recovery = "Check the Gemini CLI flags and worker configuration.";
164
+ } else if (category === "input") {
165
+ recovery = "Check the Gemini request payload and streamed event format.";
108
166
  }
109
167
 
110
168
  return Object.freeze({
111
- code: error?.code || 'GEMINI_EXECUTION_ERROR',
169
+ code: error?.code || "GEMINI_EXECUTION_ERROR",
112
170
  retryable,
113
171
  attempts,
114
172
  category,
@@ -120,10 +178,10 @@ function buildGeminiErrorInfo(error, attempts) {
120
178
  * Gemini stream-json 래퍼
121
179
  */
122
180
  export class GeminiWorker {
123
- type = 'gemini';
181
+ type = "gemini";
124
182
 
125
183
  constructor(options = {}) {
126
- this.command = options.command || 'gemini';
184
+ this.command = options.command || "gemini";
127
185
  this.commandArgs = toStringList(options.commandArgs || options.args);
128
186
  this.cwd = options.cwd || process.cwd();
129
187
  this.env = { ...process.env, ...(options.env || {}) };
@@ -132,19 +190,26 @@ export class GeminiWorker {
132
190
  this.yolo = options.yolo !== false;
133
191
  this.allowedMcpServerNames = toStringList(options.allowedMcpServerNames);
134
192
  this.extraArgs = toStringList(options.extraArgs);
135
- this.timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DEFAULT_TIMEOUT_MS;
136
- this.killGraceMs = Number(options.killGraceMs) > 0 ? Number(options.killGraceMs) : DEFAULT_KILL_GRACE_MS;
193
+ this.timeoutMs =
194
+ Number(options.timeoutMs) > 0
195
+ ? Number(options.timeoutMs)
196
+ : DEFAULT_TIMEOUT_MS;
197
+ this.killGraceMs =
198
+ Number(options.killGraceMs) > 0
199
+ ? Number(options.killGraceMs)
200
+ : DEFAULT_KILL_GRACE_MS;
137
201
  this.retryOptions = normalizeRetryOptions(options.retryOptions);
138
- this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
202
+ this.onEvent =
203
+ typeof options.onEvent === "function" ? options.onEvent : null;
139
204
 
140
- this.state = 'idle';
205
+ this.state = "idle";
141
206
  this.child = null;
142
207
  this.lastRun = null;
143
208
  }
144
209
 
145
210
  getStatus() {
146
211
  return {
147
- type: 'gemini',
212
+ type: "gemini",
148
213
  state: this.state,
149
214
  pid: this.child?.pid || null,
150
215
  last_run_at_ms: this.lastRun?.finishedAtMs || null,
@@ -153,42 +218,47 @@ export class GeminiWorker {
153
218
  }
154
219
 
155
220
  async start() {
156
- if (this.state === 'stopped') {
157
- this.state = 'idle';
221
+ if (this.state === "stopped") {
222
+ this.state = "idle";
158
223
  }
159
224
  return this.getStatus();
160
225
  }
161
226
 
162
227
  async stop() {
163
228
  if (!this.child) {
164
- this.state = 'stopped';
229
+ this.state = "stopped";
165
230
  return this.getStatus();
166
231
  }
167
232
  const child = this.child;
168
233
  terminateChild(child, this.killGraceMs);
169
234
  await new Promise((resolve) => {
170
- child.once('close', resolve);
235
+ child.once("close", resolve);
171
236
  setTimeout(resolve, this.killGraceMs + 50).unref?.();
172
237
  });
173
238
  this.child = null;
174
- this.state = 'stopped';
239
+ this.state = "stopped";
175
240
  return this.getStatus();
176
241
  }
177
242
 
178
243
  async restart() {
179
244
  await this.stop();
180
- this.state = 'idle';
245
+ this.state = "idle";
181
246
  return this.getStatus();
182
247
  }
183
248
 
184
249
  async run(prompt, options = {}) {
185
250
  if (this.child) {
186
- throw createWorkerError('GeminiWorker is already running', { code: 'WORKER_BUSY' });
251
+ throw createWorkerError("GeminiWorker is already running", {
252
+ code: "WORKER_BUSY",
253
+ });
187
254
  }
188
255
 
189
256
  await this.start();
190
257
 
191
- const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : this.timeoutMs;
258
+ const timeoutMs =
259
+ Number(options.timeoutMs) > 0
260
+ ? Number(options.timeoutMs)
261
+ : this.timeoutMs;
192
262
  const startedAtMs = Date.now();
193
263
  const args = [
194
264
  ...this.commandArgs,
@@ -196,22 +266,28 @@ export class GeminiWorker {
196
266
  model: options.model || this.model,
197
267
  approvalMode: options.approvalMode || this.approvalMode,
198
268
  yolo: options.yolo ?? this.yolo,
199
- allowedMcpServerNames: options.allowedMcpServerNames || this.allowedMcpServerNames,
269
+ allowedMcpServerNames:
270
+ options.allowedMcpServerNames || this.allowedMcpServerNames,
200
271
  extraArgs: options.extraArgs || this.extraArgs,
201
- promptArgument: options.promptArgument ?? '',
272
+ promptArgument: options.promptArgument ?? "",
202
273
  }),
203
274
  ];
204
275
 
205
- const child = spawn(this.command, args, {
276
+ const {
277
+ command: spawnCmd,
278
+ args: spawnArgs,
279
+ shell,
280
+ } = buildSpawnSpec(this.command, args);
281
+ const child = spawn(spawnCmd, spawnArgs, {
206
282
  cwd: options.cwd || this.cwd,
207
283
  env: { ...this.env, ...(options.env || {}) },
208
- stdio: ['pipe', 'pipe', 'pipe'],
284
+ stdio: ["pipe", "pipe", "pipe"],
209
285
  windowsHide: true,
210
- shell: process.platform === 'win32' && !isAbsolute(this.command),
286
+ shell,
211
287
  });
212
288
 
213
289
  this.child = child;
214
- this.state = 'running';
290
+ this.state = "running";
215
291
 
216
292
  const events = [];
217
293
  const stdoutLines = [];
@@ -230,7 +306,7 @@ export class GeminiWorker {
230
306
  crlfDelay: Infinity,
231
307
  });
232
308
 
233
- stdoutReader.on('line', (line) => {
309
+ stdoutReader.on("line", (line) => {
234
310
  if (!line) return;
235
311
  const event = safeJsonParse(line);
236
312
  if (!event) {
@@ -239,20 +315,22 @@ export class GeminiWorker {
239
315
  }
240
316
 
241
317
  events.push(event);
242
- if (event.type === 'error') lastErrorEvent = event;
318
+ if (event.type === "error") lastErrorEvent = event;
243
319
  if (this.onEvent) {
244
- try { this.onEvent(event); } catch {}
320
+ try {
321
+ this.onEvent(event);
322
+ } catch {}
245
323
  }
246
324
  });
247
325
 
248
- stderrReader.on('line', (line) => {
326
+ stderrReader.on("line", (line) => {
249
327
  if (!line) return;
250
328
  stderrLines.push(line);
251
329
  });
252
330
 
253
331
  const closePromise = new Promise((resolve, reject) => {
254
- child.once('error', reject);
255
- child.once('close', (code, signal) => {
332
+ child.once("error", reject);
333
+ child.once("close", (code, signal) => {
256
334
  exitCode = code;
257
335
  exitSignal = signal;
258
336
  resolve();
@@ -265,8 +343,8 @@ export class GeminiWorker {
265
343
  }, timeoutMs);
266
344
  timeout.unref?.();
267
345
 
268
- child.stdin.on('error', () => {});
269
- child.stdin.end(String(prompt ?? ''));
346
+ child.stdin.on("error", () => {});
347
+ child.stdin.end(String(prompt ?? ""));
270
348
 
271
349
  try {
272
350
  await closePromise;
@@ -277,32 +355,37 @@ export class GeminiWorker {
277
355
  if (this.child === child) {
278
356
  this.child = null;
279
357
  }
280
- this.state = 'idle';
358
+ this.state = "idle";
281
359
  }
282
360
 
283
- const resultEvent = findLastEvent(events, (event) => event?.type === 'result');
361
+ const resultEvent = findLastEvent(
362
+ events,
363
+ (event) => event?.type === "result",
364
+ );
284
365
  const response = [
285
366
  extractText(resultEvent),
286
367
  ...events
287
- .filter((event) => event?.type === 'message' || event?.type === 'assistant')
368
+ .filter(
369
+ (event) => event?.type === "message" || event?.type === "assistant",
370
+ )
288
371
  .map((event) => extractText(event))
289
372
  .filter(Boolean),
290
373
  ...stdoutLines,
291
374
  ]
292
375
  .filter(Boolean)
293
- .join('\n')
376
+ .join("\n")
294
377
  .trim();
295
378
 
296
379
  const result = {
297
- type: 'gemini',
380
+ type: "gemini",
298
381
  command: this.command,
299
382
  args,
300
383
  response,
301
384
  events,
302
385
  resultEvent,
303
386
  usage: resultEvent?.usage || null,
304
- stdout: stdoutLines.join('\n').trim(),
305
- stderr: stderrLines.join('\n').trim(),
387
+ stdout: stdoutLines.join("\n").trim(),
388
+ stderr: stderrLines.join("\n").trim(),
306
389
  exitCode,
307
390
  exitSignal,
308
391
  timedOut,
@@ -314,23 +397,27 @@ export class GeminiWorker {
314
397
 
315
398
  if (timedOut) {
316
399
  throw createWorkerError(`Gemini worker timed out after ${timeoutMs}ms`, {
317
- code: 'ETIMEDOUT',
400
+ code: "ETIMEDOUT",
318
401
  result,
319
402
  stderr: result.stderr,
320
403
  });
321
404
  }
322
405
 
323
406
  if (exitCode !== 0) {
324
- throw createWorkerError(`Gemini worker exited with code ${exitCode}`, {
325
- code: 'WORKER_EXIT',
407
+ // Build a descriptive message when stderr is empty to aid debugging
408
+ const errMsg = result.stderr
409
+ ? `Gemini worker exited with code ${exitCode}`
410
+ : `Gemini worker exited with code ${exitCode} (stderr empty, signal=${exitSignal ?? "none"}, events=${events.length}, stdout=${result.stdout.length}B)`;
411
+ throw createWorkerError(errMsg, {
412
+ code: "WORKER_EXIT",
326
413
  result,
327
414
  stderr: result.stderr,
328
415
  });
329
416
  }
330
417
 
331
418
  if (lastErrorEvent) {
332
- throw createWorkerError('Gemini worker emitted an error event', {
333
- code: 'WORKER_EVENT_ERROR',
419
+ throw createWorkerError("Gemini worker emitted an error event", {
420
+ code: "WORKER_EVENT_ERROR",
334
421
  result,
335
422
  stderr: result.stderr,
336
423
  });
@@ -340,20 +427,23 @@ export class GeminiWorker {
340
427
  }
341
428
 
342
429
  isReady() {
343
- return this.state !== 'stopped';
430
+ return this.state !== "stopped";
344
431
  }
345
432
 
346
433
  async execute(prompt, options = {}) {
347
434
  let attempts = 0;
348
435
 
349
436
  try {
350
- const result = await withRetry(async () => {
351
- attempts += 1;
352
- return this.run(prompt, options);
353
- }, {
354
- ...this.retryOptions,
355
- shouldRetry: (error) => isGeminiRetryable(error),
356
- });
437
+ const result = await withRetry(
438
+ async () => {
439
+ attempts += 1;
440
+ return this.run(prompt, options);
441
+ },
442
+ {
443
+ ...this.retryOptions,
444
+ shouldRetry: (error) => isGeminiRetryable(error),
445
+ },
446
+ );
357
447
 
358
448
  return {
359
449
  output: result.response,
@@ -363,8 +453,8 @@ export class GeminiWorker {
363
453
  };
364
454
  } catch (error) {
365
455
  return {
366
- output: error.stderr || error.message || 'Gemini worker failed',
367
- exitCode: error.code === 'ETIMEDOUT' ? 124 : 1,
456
+ output: error.stderr || error.message || "Gemini worker failed",
457
+ exitCode: error.code === "ETIMEDOUT" ? 124 : 1,
368
458
  sessionKey: options.sessionKey || null,
369
459
  error: buildGeminiErrorInfo(error, attempts || 1),
370
460
  raw: error.result || null,
@@ -372,3 +462,6 @@ export class GeminiWorker {
372
462
  }
373
463
  }
374
464
  }
465
+
466
+ /** @visibleForTesting — 테스트 전용 export. 외부 소비 금지. */
467
+ export { buildSpawnSpec, quoteWindowsCmdArg };
@@ -16,6 +16,21 @@
16
16
  * @property {string} [developerInstructions] - 개발자 지침
17
17
  * @property {string} [compactPrompt] - 컴팩션 프롬프트
18
18
  * @property {number} [timeoutMs] - MCP 요청 타임아웃(ms)
19
+ * @property {'mcp'|'app-server'} [transport] - Codex 전송 계층 선택 (팩토리 전용)
20
+ */
21
+
22
+ /**
23
+ * Codex 워커 생성 옵션 (팩토리 레벨)
24
+ * @typedef {object} CodexWorkerFactoryOptions
25
+ * @property {'mcp'|'app-server'} [transport] - 'mcp' (기본) 또는 'app-server'
26
+ * @property {(publishMessage: object) => void|Promise<void>} [publishCallback] - app-server 전용 publish 후크
27
+ * @property {(path: string, opts?: object) => Promise<unknown>} [requestJsonFn] - 테스트/리팩터용 requestJson 주입
28
+ * @property {string} [command] - codex 실행 바이너리 경로
29
+ * @property {string[]} [args] - codex CLI 인자 배열
30
+ * @property {string} [cwd] - 워커 작업 디렉터리
31
+ * @property {Record<string, string>} [env] - 추가 환경 변수
32
+ * @property {{ name: string, version: string }} [clientInfo] - MCP/JSON-RPC clientInfo
33
+ * @property {number} [bootstrapTimeoutMs] - 핸드셰이크 타임아웃(ms)
19
34
  */
20
35
 
21
36
  /**
@@ -46,7 +61,13 @@
46
61
  * @property {() => Promise<void>} start
47
62
  * @property {() => Promise<void>} stop
48
63
  * @property {() => boolean} isReady
49
- * @property {string} type - 'codex' | 'gemini' | 'claude' | 'delegator'
64
+ * @property {'codex'|'codex-app-server'|'gemini'|'claude'|'delegator'} type
50
65
  */
51
66
 
52
- export const WORKER_TYPES = Object.freeze(['codex', 'gemini', 'claude', 'delegator']);
67
+ export const WORKER_TYPES = Object.freeze([
68
+ "codex",
69
+ "codex-app-server",
70
+ "gemini",
71
+ "claude",
72
+ "delegator",
73
+ ]);