@triflux/remote 10.1.0 → 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 +61 -0
  3. package/hub/server.mjs +2165 -708
  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 +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 +125 -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 +44 -16
  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 +20 -118
  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 +1020 -279
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +68 -52
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +61 -33
  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 +1522 -193
  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 +18 -34
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -44
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +927 -437
  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 +517 -63
  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 +662 -335
  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,267 @@
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 } 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
+ // Escalation chain (2026-04-25 정책):
42
+ // 1. gpt-5.4-mini — 비용 최저, fast tier, 단순 task 대부분 해결
43
+ // 2. gpt-5.3-codex — 가성비 중간, code specialized (Plus/free 모두 OK, fast 미지원)
44
+ // 3. gpt-5.5 — top reasoning + 코드 강함, fast tier
45
+ // 4. claude opus-4-7 — 최종 수단
46
+ // sonnet-4-6 단계는 제거: gpt-5.5 가 코드/추론/비용 모두 우위 + 5.3-codex 가성비
47
+ // 단계가 더 적합한 중간 격상.
48
+ const DEFAULT_ESCALATION_CHAIN = Object.freeze([
49
+ Object.freeze({ cli: "codex", model: "gpt-5.4-mini" }),
50
+ Object.freeze({ cli: "codex", model: "gpt-5.3-codex" }),
51
+ Object.freeze({ cli: "codex", model: "gpt-5.5" }),
52
+ Object.freeze({ cli: "claude", model: "opus-4-7" }),
53
+ ]);
54
+
55
+ const STUCK_THRESHOLD = 3;
56
+
57
+ export function createRetryStateMachine(options = {}) {
58
+ const mode = options.mode || MODES.BOUNDED;
59
+ const cliChain = Array.isArray(options.cliChain)
60
+ ? options.cliChain.slice()
61
+ : DEFAULT_ESCALATION_CHAIN.slice();
62
+ const maxIterationsInput =
63
+ typeof options.maxIterations === "number" ? options.maxIterations : null;
64
+ const maxIterations =
65
+ maxIterationsInput !== null
66
+ ? maxIterationsInput
67
+ : mode === MODES.BOUNDED
68
+ ? 3
69
+ : 0;
70
+ const stateFile = options.stateFile || null;
71
+ const sessionId = options.sessionId || null;
72
+
73
+ const emitter = new EventEmitter();
74
+ const state = {
75
+ current: STATES.PLANNING,
76
+ iterations: 0,
77
+ maxIterations,
78
+ stuckCounter: 0,
79
+ lastFailureReason: null,
80
+ cliIndex: 0,
81
+ cliChain,
82
+ mode,
83
+ sessionId,
84
+ stateFile,
85
+ history: [],
86
+ };
87
+
88
+ function transition(next, meta = {}) {
89
+ const prev = state.current;
90
+ state.current = next;
91
+ const entry = {
92
+ t: Date.now(),
93
+ from: prev,
94
+ to: next,
95
+ iteration: state.iterations,
96
+ cliIndex: state.cliIndex,
97
+ ...meta,
98
+ };
99
+ state.history.push(entry);
100
+ if (stateFile) persistTransition(stateFile, entry);
101
+ emitter.emit("transition", entry);
102
+ return entry;
103
+ }
104
+
105
+ function startIteration() {
106
+ state.iterations += 1;
107
+ return transition(STATES.EXECUTING, { iteration: state.iterations });
108
+ }
109
+
110
+ function reportVerifySuccess() {
111
+ return transition(STATES.DONE);
112
+ }
113
+
114
+ function reportVerifyFail(failureReason) {
115
+ const reason = String(failureReason || "unknown");
116
+ if (reason === state.lastFailureReason) {
117
+ state.stuckCounter += 1;
118
+ } else {
119
+ state.stuckCounter = 1;
120
+ state.lastFailureReason = reason;
121
+ }
122
+
123
+ if (state.stuckCounter >= STUCK_THRESHOLD) {
124
+ return transition(STATES.STUCK, {
125
+ reason,
126
+ stuckCounter: state.stuckCounter,
127
+ });
128
+ }
129
+
130
+ if (state.maxIterations > 0 && state.iterations >= state.maxIterations) {
131
+ if (state.mode === MODES.ESCALATE) {
132
+ return escalate();
133
+ }
134
+ return transition(STATES.BUDGET_EXCEEDED, {
135
+ reason,
136
+ iterations: state.iterations,
137
+ });
138
+ }
139
+
140
+ return transition(STATES.DIAGNOSING, { reason });
141
+ }
142
+
143
+ function escalate() {
144
+ if (state.cliIndex + 1 >= state.cliChain.length) {
145
+ return transition(STATES.BUDGET_EXCEEDED, {
146
+ reason: "escalation-chain-exhausted",
147
+ chain: state.cliChain,
148
+ });
149
+ }
150
+ state.cliIndex += 1;
151
+ state.iterations = 0;
152
+ state.stuckCounter = 0;
153
+ state.lastFailureReason = null;
154
+ return transition(STATES.EXECUTING, {
155
+ cli: state.cliChain[state.cliIndex],
156
+ escalated: true,
157
+ });
158
+ }
159
+
160
+ function getCurrent() {
161
+ return {
162
+ current: state.current,
163
+ iterations: state.iterations,
164
+ maxIterations: state.maxIterations,
165
+ stuckCounter: state.stuckCounter,
166
+ lastFailureReason: state.lastFailureReason,
167
+ cliIndex: state.cliIndex,
168
+ cliChain: state.cliChain.slice(),
169
+ mode: state.mode,
170
+ sessionId: state.sessionId,
171
+ history: state.history.slice(),
172
+ };
173
+ }
174
+
175
+ function on(event, listener) {
176
+ emitter.on(event, listener);
177
+ return () => emitter.off(event, listener);
178
+ }
179
+
180
+ function serialize() {
181
+ return {
182
+ version: 1,
183
+ current: state.current,
184
+ iterations: state.iterations,
185
+ maxIterations: state.maxIterations,
186
+ stuckCounter: state.stuckCounter,
187
+ lastFailureReason: state.lastFailureReason,
188
+ cliIndex: state.cliIndex,
189
+ cliChain: state.cliChain.slice(),
190
+ mode: state.mode,
191
+ sessionId: state.sessionId,
192
+ history: state.history.slice(),
193
+ };
194
+ }
195
+
196
+ function applySnapshot(snapshot) {
197
+ if (!snapshot || typeof snapshot !== "object") return;
198
+ state.current = snapshot.current || STATES.PLANNING;
199
+ state.iterations = Number(snapshot.iterations) || 0;
200
+ state.maxIterations =
201
+ snapshot.maxIterations !== undefined
202
+ ? Number(snapshot.maxIterations)
203
+ : state.maxIterations;
204
+ state.stuckCounter = Number(snapshot.stuckCounter) || 0;
205
+ state.lastFailureReason = snapshot.lastFailureReason || null;
206
+ state.cliIndex = Number(snapshot.cliIndex) || 0;
207
+ if (Array.isArray(snapshot.cliChain) && snapshot.cliChain.length > 0) {
208
+ state.cliChain = snapshot.cliChain.slice();
209
+ }
210
+ if (snapshot.mode) state.mode = snapshot.mode;
211
+ if (snapshot.sessionId !== undefined) state.sessionId = snapshot.sessionId;
212
+ if (Array.isArray(snapshot.history))
213
+ state.history = snapshot.history.slice();
214
+ }
215
+
216
+ return {
217
+ STATES,
218
+ MODES,
219
+ getCurrent,
220
+ startIteration,
221
+ reportVerifySuccess,
222
+ reportVerifyFail,
223
+ escalate,
224
+ on,
225
+ serialize,
226
+ applySnapshot,
227
+ };
228
+ }
229
+
230
+ function persistTransition(stateFile, entry) {
231
+ const dir = dirname(stateFile);
232
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
233
+ appendFileSync(stateFile, `${JSON.stringify(entry)}\n`, "utf8");
234
+ }
235
+
236
+ export function resumeFromStateFile(stateFile) {
237
+ if (!existsSync(stateFile)) return null;
238
+ const raw = readFileSync(stateFile, "utf8").trim();
239
+ if (!raw) return null;
240
+ const lines = raw.split("\n").filter(Boolean);
241
+ if (lines.length === 0) return null;
242
+ return JSON.parse(lines[lines.length - 1]);
243
+ }
244
+
245
+ // Full-snapshot 기반 stateFile I/O — bridge retry-run 에서 multi-process state 복원용.
246
+ // transition event log (resumeFromStateFile) 와 별도 파일로 관리하는 것을 권장.
247
+ export function loadSnapshot(snapshotFile) {
248
+ if (!existsSync(snapshotFile)) return null;
249
+ const raw = readFileSync(snapshotFile, "utf8").trim();
250
+ if (!raw) return null;
251
+ const parsed = JSON.parse(raw);
252
+ if (!parsed || typeof parsed !== "object") return null;
253
+ if (parsed.version !== 1) {
254
+ throw new Error(
255
+ `unsupported snapshot version: ${parsed.version} (expected 1)`,
256
+ );
257
+ }
258
+ return parsed;
259
+ }
260
+
261
+ export function saveSnapshot(snapshotFile, snapshot) {
262
+ const dir = dirname(snapshotFile);
263
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
264
+ writeFileSync(snapshotFile, JSON.stringify(snapshot), "utf8");
265
+ }
266
+
267
+ 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
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "WorkerCompletion",
4
+ "description": "Self-reported worker completion payload emitted by a swarm shard worker.",
5
+ "type": "object",
6
+ "required": ["status"],
7
+ "properties": {
8
+ "status": {
9
+ "type": "string",
10
+ "enum": ["ok", "skipped", "failed"]
11
+ },
12
+ "shard": { "type": "string" },
13
+ "reason": { "type": "string" },
14
+ "commits_made": {
15
+ "type": "array",
16
+ "items": {
17
+ "type": "object",
18
+ "required": ["sha"],
19
+ "properties": {
20
+ "sha": { "type": "string", "minLength": 7 },
21
+ "message": { "type": "string" }
22
+ },
23
+ "additionalProperties": true
24
+ }
25
+ }
26
+ },
27
+ "additionalProperties": true,
28
+ "allOf": [
29
+ {
30
+ "if": { "properties": { "status": { "const": "ok" } } },
31
+ "then": {
32
+ "required": ["commits_made"],
33
+ "properties": {
34
+ "commits_made": { "minItems": 1 }
35
+ }
36
+ }
37
+ }
38
+ ]
39
+ }