@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,336 @@
1
+ // hub/team/retry-state-machine.mjs
2
+ // Phase 3 Step A — true ralph / auto-escalate retry state machine.
3
+ // 설계 문서: .triflux/plans/phase3-lead-codex-ralph-escalate.md
4
+ //
5
+ // Modes:
6
+ // bounded — --retry 1 (기본): maxIterations 도달 시 BUDGET_EXCEEDED.
7
+ // ralph — --retry ralph: maxIterations=0 이면 unlimited. stuck detector 만 종료.
8
+ // auto-escalate — --retry auto-escalate: BUDGET_EXCEEDED 시 다음 CLI 로 전이.
9
+ //
10
+ // 상태 전이:
11
+ // PLANNING → EXECUTING → VERIFYING.success → DONE
12
+ // → VERIFYING.fail → DIAGNOSING → EXECUTING …
13
+ // → stuckCounter≥3 → STUCK
14
+ // → iter≥max → BUDGET_EXCEEDED (escalate 모드는 다음 CLI)
15
+
16
+ import { EventEmitter } from "node:events";
17
+ import {
18
+ appendFileSync,
19
+ existsSync,
20
+ mkdirSync,
21
+ readFileSync,
22
+ writeFileSync,
23
+ } from "node:fs";
24
+ import { dirname, join } from "node:path";
25
+
26
+ export const STATES = Object.freeze({
27
+ PLANNING: "PLANNING",
28
+ EXECUTING: "EXECUTING",
29
+ DIAGNOSING: "DIAGNOSING",
30
+ STUCK: "STUCK",
31
+ BUDGET_EXCEEDED: "BUDGET_EXCEEDED",
32
+ DONE: "DONE",
33
+ });
34
+
35
+ export const MODES = Object.freeze({
36
+ BOUNDED: "bounded",
37
+ RALPH: "ralph",
38
+ ESCALATE: "auto-escalate",
39
+ });
40
+
41
+ const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
42
+
43
+ // Escalation chain (2026-05-27 정책):
44
+ // 1. codex gpt-5.5 — 코드/추론/비용에서 mini/5.3-codex 중간 단계를 대체
45
+ // 2. claude opus-4-7 — 최종 수단
46
+ const DEFAULT_ESCALATION_CHAIN = Object.freeze([
47
+ Object.freeze({ cli: "codex", model: "gpt-5.5" }),
48
+ Object.freeze({ cli: "claude", model: "opus-4-7" }),
49
+ ]);
50
+
51
+ const STUCK_THRESHOLD = 3;
52
+
53
+ export function createRetryStateMachine(options = {}) {
54
+ const mode = options.mode || MODES.BOUNDED;
55
+ const cliChain = resolveEscalationChain(options);
56
+ const maxIterationsInput =
57
+ typeof options.maxIterations === "number" ? options.maxIterations : null;
58
+ const maxIterations =
59
+ maxIterationsInput !== null
60
+ ? maxIterationsInput
61
+ : mode === MODES.BOUNDED
62
+ ? 3
63
+ : 0;
64
+ const stateFile = options.stateFile || null;
65
+ const sessionId = options.sessionId || null;
66
+
67
+ const emitter = new EventEmitter();
68
+ const state = {
69
+ current: STATES.PLANNING,
70
+ iterations: 0,
71
+ maxIterations,
72
+ stuckCounter: 0,
73
+ lastFailureReason: null,
74
+ cliIndex: 0,
75
+ cliChain,
76
+ mode,
77
+ sessionId,
78
+ stateFile,
79
+ history: [],
80
+ };
81
+
82
+ function transition(next, meta = {}) {
83
+ const prev = state.current;
84
+ state.current = next;
85
+ const entry = {
86
+ t: Date.now(),
87
+ from: prev,
88
+ to: next,
89
+ iteration: state.iterations,
90
+ cliIndex: state.cliIndex,
91
+ ...meta,
92
+ };
93
+ state.history.push(entry);
94
+ if (stateFile) persistTransition(stateFile, entry);
95
+ emitter.emit("transition", entry);
96
+ return entry;
97
+ }
98
+
99
+ function startIteration() {
100
+ state.iterations += 1;
101
+ return transition(STATES.EXECUTING, { iteration: state.iterations });
102
+ }
103
+
104
+ function reportVerifySuccess() {
105
+ return transition(STATES.DONE);
106
+ }
107
+
108
+ function reportVerifyFail(failureReason) {
109
+ const reason = String(failureReason || "unknown");
110
+ if (reason === state.lastFailureReason) {
111
+ state.stuckCounter += 1;
112
+ } else {
113
+ state.stuckCounter = 1;
114
+ state.lastFailureReason = reason;
115
+ }
116
+
117
+ if (state.stuckCounter >= STUCK_THRESHOLD) {
118
+ return transition(STATES.STUCK, {
119
+ reason,
120
+ stuckCounter: state.stuckCounter,
121
+ });
122
+ }
123
+
124
+ if (state.maxIterations > 0 && state.iterations >= state.maxIterations) {
125
+ if (state.mode === MODES.ESCALATE) {
126
+ return escalate();
127
+ }
128
+ return transition(STATES.BUDGET_EXCEEDED, {
129
+ reason,
130
+ iterations: state.iterations,
131
+ });
132
+ }
133
+
134
+ return transition(STATES.DIAGNOSING, { reason });
135
+ }
136
+
137
+ function escalate() {
138
+ if (state.cliIndex + 1 >= state.cliChain.length) {
139
+ return transition(STATES.BUDGET_EXCEEDED, {
140
+ reason: "escalation-chain-exhausted",
141
+ chain: state.cliChain,
142
+ });
143
+ }
144
+ state.cliIndex += 1;
145
+ state.iterations = 0;
146
+ state.stuckCounter = 0;
147
+ state.lastFailureReason = null;
148
+ return transition(STATES.EXECUTING, {
149
+ cli: state.cliChain[state.cliIndex],
150
+ escalated: true,
151
+ });
152
+ }
153
+
154
+ function getCurrent() {
155
+ return {
156
+ current: state.current,
157
+ iterations: state.iterations,
158
+ maxIterations: state.maxIterations,
159
+ stuckCounter: state.stuckCounter,
160
+ lastFailureReason: state.lastFailureReason,
161
+ cliIndex: state.cliIndex,
162
+ cliChain: state.cliChain.slice(),
163
+ mode: state.mode,
164
+ sessionId: state.sessionId,
165
+ history: state.history.slice(),
166
+ };
167
+ }
168
+
169
+ function on(event, listener) {
170
+ emitter.on(event, listener);
171
+ return () => emitter.off(event, listener);
172
+ }
173
+
174
+ function serialize() {
175
+ return {
176
+ version: 1,
177
+ current: state.current,
178
+ iterations: state.iterations,
179
+ maxIterations: state.maxIterations,
180
+ stuckCounter: state.stuckCounter,
181
+ lastFailureReason: state.lastFailureReason,
182
+ cliIndex: state.cliIndex,
183
+ cliChain: state.cliChain.slice(),
184
+ mode: state.mode,
185
+ sessionId: state.sessionId,
186
+ history: state.history.slice(),
187
+ };
188
+ }
189
+
190
+ function applySnapshot(snapshot) {
191
+ if (!snapshot || typeof snapshot !== "object") return;
192
+ state.current = snapshot.current || STATES.PLANNING;
193
+ state.iterations = Number(snapshot.iterations) || 0;
194
+ state.maxIterations =
195
+ snapshot.maxIterations !== undefined
196
+ ? Number(snapshot.maxIterations)
197
+ : state.maxIterations;
198
+ state.stuckCounter = Number(snapshot.stuckCounter) || 0;
199
+ state.lastFailureReason = snapshot.lastFailureReason || null;
200
+ state.cliIndex = Number(snapshot.cliIndex) || 0;
201
+ if (Array.isArray(snapshot.cliChain) && snapshot.cliChain.length > 0) {
202
+ state.cliChain = normalizeEscalationChain(
203
+ snapshot.cliChain,
204
+ "snapshot.cliChain",
205
+ );
206
+ }
207
+ if (snapshot.mode) state.mode = snapshot.mode;
208
+ if (snapshot.sessionId !== undefined) state.sessionId = snapshot.sessionId;
209
+ if (Array.isArray(snapshot.history))
210
+ state.history = snapshot.history.slice();
211
+ }
212
+
213
+ return {
214
+ STATES,
215
+ MODES,
216
+ getCurrent,
217
+ startIteration,
218
+ reportVerifySuccess,
219
+ reportVerifyFail,
220
+ escalate,
221
+ on,
222
+ serialize,
223
+ applySnapshot,
224
+ };
225
+ }
226
+
227
+ function resolveEscalationChain(options) {
228
+ if (Array.isArray(options.cliChain)) {
229
+ return normalizeEscalationChain(options.cliChain, "options.cliChain");
230
+ }
231
+
232
+ const projectRoot = options.projectRoot || process.cwd();
233
+ return (
234
+ loadEscalationChainOverride(projectRoot) ||
235
+ normalizeEscalationChain(
236
+ DEFAULT_ESCALATION_CHAIN,
237
+ "DEFAULT_ESCALATION_CHAIN",
238
+ )
239
+ );
240
+ }
241
+
242
+ export function loadEscalationChainOverride(projectRoot = process.cwd()) {
243
+ const configPath = join(projectRoot, ESCALATION_CHAIN_CONFIG_PATH);
244
+ if (!existsSync(configPath)) return null;
245
+
246
+ const raw = readFileSync(configPath, "utf8");
247
+ let parsed;
248
+ try {
249
+ parsed = JSON.parse(raw);
250
+ } catch (err) {
251
+ throw new Error(
252
+ `escalation-chain override 파싱 실패: ${configPath} — 원인: ${err.message} — 수정: JSON 문법 확인 또는 파일 삭제로 DEFAULT_ESCALATION_CHAIN 사용`,
253
+ { cause: err },
254
+ );
255
+ }
256
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
257
+ throw new Error(`${configPath}: expected object`);
258
+ }
259
+ if (parsed.version !== undefined && parsed.version !== 1) {
260
+ throw new Error(`${configPath}: unsupported version ${parsed.version}`);
261
+ }
262
+ return normalizeEscalationChain(parsed.chain, configPath);
263
+ }
264
+
265
+ function normalizeEscalationChain(chain, source) {
266
+ if (!Array.isArray(chain) || chain.length === 0) {
267
+ throw new Error(`${source}: chain must be a non-empty array`);
268
+ }
269
+ return chain.map((entry, index) =>
270
+ normalizeEscalationChainEntry(entry, index, source),
271
+ );
272
+ }
273
+
274
+ function normalizeEscalationChainEntry(entry, index, source) {
275
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
276
+ throw new Error(`${source}: chain[${index}] must be an object`);
277
+ }
278
+ if (typeof entry.cli !== "string" || entry.cli.trim() === "") {
279
+ throw new Error(
280
+ `${source}: chain[${index}].cli must be a non-empty string`,
281
+ );
282
+ }
283
+ if (typeof entry.model !== "string" || entry.model.trim() === "") {
284
+ throw new Error(
285
+ `${source}: chain[${index}].model must be a non-empty string`,
286
+ );
287
+ }
288
+
289
+ const normalized = {
290
+ cli: entry.cli,
291
+ model: entry.model,
292
+ };
293
+ if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
294
+ normalized.profile = entry.profile;
295
+ }
296
+ return normalized;
297
+ }
298
+
299
+ function persistTransition(stateFile, entry) {
300
+ const dir = dirname(stateFile);
301
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
302
+ appendFileSync(stateFile, `${JSON.stringify(entry)}\n`, "utf8");
303
+ }
304
+
305
+ export function resumeFromStateFile(stateFile) {
306
+ if (!existsSync(stateFile)) return null;
307
+ const raw = readFileSync(stateFile, "utf8").trim();
308
+ if (!raw) return null;
309
+ const lines = raw.split("\n").filter(Boolean);
310
+ if (lines.length === 0) return null;
311
+ return JSON.parse(lines[lines.length - 1]);
312
+ }
313
+
314
+ // Full-snapshot 기반 stateFile I/O — bridge retry-run 에서 multi-process state 복원용.
315
+ // transition event log (resumeFromStateFile) 와 별도 파일로 관리하는 것을 권장.
316
+ export function loadSnapshot(snapshotFile) {
317
+ if (!existsSync(snapshotFile)) return null;
318
+ const raw = readFileSync(snapshotFile, "utf8").trim();
319
+ if (!raw) return null;
320
+ const parsed = JSON.parse(raw);
321
+ if (!parsed || typeof parsed !== "object") return null;
322
+ if (parsed.version !== 1) {
323
+ throw new Error(
324
+ `unsupported snapshot version: ${parsed.version} (expected 1)`,
325
+ );
326
+ }
327
+ return parsed;
328
+ }
329
+
330
+ export function saveSnapshot(snapshotFile, snapshot) {
331
+ const dir = dirname(snapshotFile);
332
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
333
+ writeFileSync(snapshotFile, JSON.stringify(snapshot), "utf8");
334
+ }
335
+
336
+ export { DEFAULT_ESCALATION_CHAIN, STUCK_THRESHOLD };
@@ -15,31 +15,55 @@
15
15
  export function resolveRoutingStrategy({ subtasks, graph_type, thorough }) {
16
16
  const N = subtasks.length;
17
17
  if (N === 0) {
18
- const dagContext = { dag_width: 0, levels: {}, edges: [], max_complexity: 'S', taskResults: {} };
19
- return { strategy: 'quick_single', reason: 'empty_subtasks', dag_width: 0, max_complexity: 'S', dagContext };
18
+ const dagContext = {
19
+ dag_width: 0,
20
+ levels: {},
21
+ edges: [],
22
+ max_complexity: "S",
23
+ taskResults: {},
24
+ };
25
+ return {
26
+ strategy: "quick_single",
27
+ reason: "empty_subtasks",
28
+ dag_width: 0,
29
+ max_complexity: "S",
30
+ dagContext,
31
+ };
20
32
  }
21
33
 
22
- const { width: dag_width, levels, edges } = computeDagInfo(subtasks, graph_type);
34
+ const {
35
+ width: dag_width,
36
+ levels,
37
+ edges,
38
+ } = computeDagInfo(subtasks, graph_type);
23
39
  const max_complexity = getMaxComplexity(subtasks);
24
- const dagContext = { dag_width, levels, edges, max_complexity, taskResults: {} };
25
- const isHighComplexity = ['L', 'XL'].includes(max_complexity);
40
+ const dagContext = {
41
+ dag_width,
42
+ levels,
43
+ edges,
44
+ max_complexity,
45
+ taskResults: {},
46
+ };
47
+ const isHighComplexity = ["L", "XL"].includes(max_complexity);
26
48
  const allSameAgent = new Set(subtasks.map((s) => s.agent)).size === 1;
27
- const allSmall = subtasks.every((s) => normalizeComplexity(s.complexity) === 'S');
49
+ const allSmall = subtasks.every(
50
+ (s) => normalizeComplexity(s.complexity) === "S",
51
+ );
28
52
 
29
53
  // N==1: 단일 태스크
30
54
  if (N === 1) {
31
55
  if (thorough || isHighComplexity) {
32
56
  return {
33
- strategy: 'thorough_single',
34
- reason: 'single_high_complexity',
57
+ strategy: "thorough_single",
58
+ reason: "single_high_complexity",
35
59
  dag_width,
36
60
  max_complexity,
37
61
  dagContext,
38
62
  };
39
63
  }
40
64
  return {
41
- strategy: 'quick_single',
42
- reason: 'single_low_complexity',
65
+ strategy: "quick_single",
66
+ reason: "single_low_complexity",
43
67
  dag_width,
44
68
  max_complexity,
45
69
  dagContext,
@@ -50,16 +74,16 @@ export function resolveRoutingStrategy({ subtasks, graph_type, thorough }) {
50
74
  if (dag_width === 1) {
51
75
  if (thorough || isHighComplexity) {
52
76
  return {
53
- strategy: 'thorough_single',
54
- reason: 'sequential_chain',
77
+ strategy: "thorough_single",
78
+ reason: "sequential_chain",
55
79
  dag_width,
56
80
  max_complexity,
57
81
  dagContext,
58
82
  };
59
83
  }
60
84
  return {
61
- strategy: 'quick_single',
62
- reason: 'sequential_chain',
85
+ strategy: "quick_single",
86
+ reason: "sequential_chain",
63
87
  dag_width,
64
88
  max_complexity,
65
89
  dagContext,
@@ -69,8 +93,8 @@ export function resolveRoutingStrategy({ subtasks, graph_type, thorough }) {
69
93
  // 동일 에이전트 + 모두 S: 프롬프트 병합 -> batch single
70
94
  if (allSameAgent && allSmall) {
71
95
  return {
72
- strategy: 'batch_single',
73
- reason: 'same_agent_small_batch',
96
+ strategy: "batch_single",
97
+ reason: "same_agent_small_batch",
74
98
  dag_width,
75
99
  max_complexity,
76
100
  dagContext,
@@ -80,16 +104,16 @@ export function resolveRoutingStrategy({ subtasks, graph_type, thorough }) {
80
104
  // dag_width >= 2: 팀
81
105
  if (thorough || isHighComplexity) {
82
106
  return {
83
- strategy: 'thorough_team',
84
- reason: 'parallel_high_complexity',
107
+ strategy: "thorough_team",
108
+ reason: "parallel_high_complexity",
85
109
  dag_width,
86
110
  max_complexity,
87
111
  dagContext,
88
112
  };
89
113
  }
90
114
  return {
91
- strategy: 'quick_team',
92
- reason: 'parallel_low_complexity',
115
+ strategy: "quick_team",
116
+ reason: "parallel_low_complexity",
93
117
  dag_width,
94
118
  max_complexity,
95
119
  dagContext,
@@ -103,7 +127,7 @@ export function resolveRoutingStrategy({ subtasks, graph_type, thorough }) {
103
127
  * @returns {{ width: number, levels: Record<number, string[]>, edges: Array<{from:string, to:string}> }}
104
128
  */
105
129
  function computeDagInfo(subtasks, graph_type) {
106
- if (graph_type === 'SEQUENTIAL') {
130
+ if (graph_type === "SEQUENTIAL") {
107
131
  const levels = {};
108
132
  const edges = [];
109
133
  subtasks.forEach((t, i) => {
@@ -112,7 +136,7 @@ function computeDagInfo(subtasks, graph_type) {
112
136
  });
113
137
  return { width: 1, levels, edges };
114
138
  }
115
- if (graph_type === 'INDEPENDENT') {
139
+ if (graph_type === "INDEPENDENT") {
116
140
  const levels = { 0: subtasks.map((t) => t.id) };
117
141
  return { width: subtasks.length, levels, edges: [] };
118
142
  }
@@ -173,7 +197,9 @@ function computeDagInfo(subtasks, graph_type) {
173
197
  export function getUpstreamResults(taskId, pipelineState) {
174
198
  const ctx = pipelineState?.dagContext;
175
199
  if (!ctx) return {};
176
- const upstreamIds = ctx.edges.filter((e) => e.to === taskId).map((e) => e.from);
200
+ const upstreamIds = ctx.edges
201
+ .filter((e) => e.to === taskId)
202
+ .map((e) => e.from);
177
203
  const results = {};
178
204
  for (const id of upstreamIds) {
179
205
  if (id in (ctx.taskResults || {})) {
@@ -205,7 +231,7 @@ export function updateTaskResult(taskId, result, pipelineState) {
205
231
  */
206
232
  function getMaxComplexity(subtasks) {
207
233
  const order = { S: 0, M: 1, L: 2, XL: 3 };
208
- let max = 'S';
234
+ let max = "S";
209
235
  for (const s of subtasks) {
210
236
  const complexity = normalizeComplexity(s.complexity);
211
237
  if (order[complexity] > order[max]) max = complexity;
@@ -219,5 +245,5 @@ function getMaxComplexity(subtasks) {
219
245
  * @returns {"S" | "M" | "L" | "XL"}
220
246
  */
221
247
  function normalizeComplexity(complexity) {
222
- return ['S', 'M', 'L', 'XL'].includes(complexity) ? complexity : 'M';
248
+ return ["S", "M", "L", "XL"].includes(complexity) ? complexity : "M";
223
249
  }
@@ -0,0 +1,200 @@
1
+ import {
2
+ createPsmuxSession,
3
+ killPsmuxSession,
4
+ psmuxSessionExists,
5
+ } from "./psmux.mjs";
6
+ import { tmuxExec } from "./session.mjs";
7
+
8
+ /**
9
+ * @typedef {object} RuntimeStatus
10
+ * @property {string} name
11
+ * @property {string} sessionName
12
+ * @property {boolean} alive
13
+ */
14
+
15
+ /**
16
+ * @typedef {object} TeamRuntime
17
+ * @property {(sessionName: string, opts?: object) => unknown} start
18
+ * @property {(sessionName: string) => void} stop
19
+ * @property {(sessionName: string) => boolean} isAlive
20
+ * @property {(sessionName: string) => RuntimeStatus} getStatus
21
+ */
22
+
23
+ const defaultPsmuxAdapter = {
24
+ createSession: createPsmuxSession,
25
+ killSession: killPsmuxSession,
26
+ hasSession: psmuxSessionExists,
27
+ };
28
+
29
+ /**
30
+ * @param {{
31
+ * createSession: typeof createPsmuxSession,
32
+ * killSession: typeof killPsmuxSession,
33
+ * hasSession: typeof psmuxSessionExists,
34
+ * }} [adapter]
35
+ * @returns {TeamRuntime & { name: "psmux" }}
36
+ */
37
+ export function createPsmuxRuntime(adapter = defaultPsmuxAdapter) {
38
+ return {
39
+ name: "psmux",
40
+ start(sessionName, opts = {}) {
41
+ return adapter.createSession(sessionName, opts);
42
+ },
43
+ stop(sessionName) {
44
+ adapter.killSession(sessionName);
45
+ },
46
+ isAlive(sessionName) {
47
+ return adapter.hasSession(sessionName);
48
+ },
49
+ getStatus(sessionName) {
50
+ return {
51
+ name: "psmux",
52
+ sessionName,
53
+ alive: adapter.hasSession(sessionName),
54
+ };
55
+ },
56
+ };
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // tmux 어댑터
61
+ // ---------------------------------------------------------------------------
62
+
63
+ // tmux 세션 이름은 tmuxExec 명령 문자열에 직접 보간되므로 shell 메타문자가 들어오면 위험.
64
+ // 현 호출처는 모두 safe identifier (slug/uuid 류) 이지만 어댑터 export 가 contract 를
65
+ // 강제하지 않아 외부 호출자가 임의 문자열을 주입할 수 있다 → 보수적으로 검증.
66
+ const TMUX_SAFE_SESSION_NAME = /^[A-Za-z0-9_.:-]+$/;
67
+
68
+ function assertSafeTmuxSessionName(sessionName) {
69
+ const value = String(sessionName ?? "");
70
+ if (!TMUX_SAFE_SESSION_NAME.test(value)) {
71
+ throw new Error(
72
+ `Unsafe tmux session name: ${JSON.stringify(value)} (allowed: A-Za-z0-9_.:-)`,
73
+ );
74
+ }
75
+ return value;
76
+ }
77
+
78
+ function tmuxSessionExists(sessionName) {
79
+ const safe = assertSafeTmuxSessionName(sessionName);
80
+ try {
81
+ tmuxExec(`has-session -t ${safe}`);
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function createTmuxSession(sessionName, opts = {}) {
89
+ const safe = assertSafeTmuxSessionName(sessionName);
90
+ tmuxExec(`new-session -d -s ${safe} -x 220 -y 55`);
91
+ }
92
+
93
+ function sendPromptToTmuxSession(sessionName, prompt) {
94
+ const safe = assertSafeTmuxSessionName(sessionName);
95
+ const quotedPrompt = String(prompt).replace(/'/g, "'\\''");
96
+ tmuxExec(`send-keys -t ${safe}:0.0 '${quotedPrompt}' Enter`);
97
+ }
98
+
99
+ function killTmuxSessionByName(sessionName) {
100
+ let safe;
101
+ try {
102
+ safe = assertSafeTmuxSessionName(sessionName);
103
+ } catch {
104
+ return;
105
+ }
106
+ try {
107
+ tmuxExec(`kill-session -t ${safe}`);
108
+ } catch {
109
+ // 이미 종료된 세션 — 무시
110
+ }
111
+ }
112
+
113
+ const defaultTmuxAdapter = {
114
+ createSession: createTmuxSession,
115
+ sendPrompt: sendPromptToTmuxSession,
116
+ killSession: killTmuxSessionByName,
117
+ hasSession: tmuxSessionExists,
118
+ };
119
+
120
+ /**
121
+ * @param {{
122
+ * createSession: typeof createTmuxSession,
123
+ * sendPrompt: typeof sendPromptToTmuxSession,
124
+ * killSession: typeof killTmuxSessionByName,
125
+ * hasSession: typeof tmuxSessionExists,
126
+ * }} [opts.adapter]
127
+ * @param {string} [opts.sessionName]
128
+ * @returns {{
129
+ * name: "tmux",
130
+ * kind: "tmux",
131
+ * sessionName: string,
132
+ * createSession: (opts?: object) => unknown,
133
+ * sendPrompt: (prompt: string) => unknown,
134
+ * start: (sessionName: string, opts?: object) => unknown,
135
+ * stop: (sessionName: string) => void,
136
+ * isAlive: (sessionName: string) => boolean,
137
+ * getStatus: (sessionName: string) => RuntimeStatus,
138
+ * }}
139
+ */
140
+ export function createTmuxRuntime(opts = {}) {
141
+ const { adapter = defaultTmuxAdapter, sessionName = "" } = opts;
142
+
143
+ return {
144
+ name: "tmux",
145
+ kind: "tmux",
146
+ sessionName,
147
+ // start() 와 동일하게 첫 인자로 sessionName override 를 받는다.
148
+ // 첫 인자가 string 이 아니면 (옵션 객체 등) closure sessionName 을 사용하는 legacy 형태로 폴백.
149
+ createSession(sessionNameArg, opts) {
150
+ if (typeof sessionNameArg === "string") {
151
+ return adapter.createSession(sessionNameArg, opts ?? {});
152
+ }
153
+ return adapter.createSession(sessionName, sessionNameArg ?? {});
154
+ },
155
+ sendPrompt(prompt, sessionNameArg = sessionName) {
156
+ return adapter.sendPrompt(sessionNameArg, prompt);
157
+ },
158
+ start(sessionNameArg = sessionName, opts = {}) {
159
+ return adapter.createSession(sessionNameArg, opts);
160
+ },
161
+ stop(sessionNameArg = sessionName) {
162
+ adapter.killSession(sessionNameArg);
163
+ },
164
+ isAlive(sessionNameArg = sessionName) {
165
+ return adapter.hasSession(sessionNameArg);
166
+ },
167
+ getStatus(sessionNameArg = sessionName) {
168
+ return {
169
+ name: "tmux",
170
+ sessionName: sessionNameArg,
171
+ alive: adapter.hasSession(sessionNameArg),
172
+ };
173
+ },
174
+ };
175
+ }
176
+
177
+ /**
178
+ * @param {string} mode
179
+ * @param {object} [opts]
180
+ * @returns {TeamRuntime & { name: string }}
181
+ */
182
+ export function createRuntime(mode, opts = {}) {
183
+ const normalizedMode = String(mode || "")
184
+ .trim()
185
+ .toLowerCase();
186
+
187
+ if (normalizedMode === "psmux") {
188
+ return createPsmuxRuntime();
189
+ }
190
+
191
+ if (normalizedMode === "tmux") {
192
+ return createTmuxRuntime(opts);
193
+ }
194
+
195
+ if (normalizedMode === "native" || normalizedMode === "wt") {
196
+ throw new Error(`Runtime mode "${normalizedMode}" is not implemented yet.`);
197
+ }
198
+
199
+ throw new Error(`Unsupported runtime mode: ${mode}`);
200
+ }