@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.
- package/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +68 -7
- package/hub/server.mjs +2181 -701
- package/hub/store-adapter.mjs +101 -37
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +127 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +45 -17
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +21 -119
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1067 -264
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +69 -53
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +62 -34
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1526 -198
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +21 -37
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -45
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +936 -449
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +518 -64
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +663 -337
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- 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 };
|
package/hub/team/routing.mjs
CHANGED
|
@@ -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 = {
|
|
19
|
-
|
|
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 {
|
|
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 = {
|
|
25
|
-
|
|
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(
|
|
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:
|
|
34
|
-
reason:
|
|
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:
|
|
42
|
-
reason:
|
|
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:
|
|
54
|
-
reason:
|
|
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:
|
|
62
|
-
reason:
|
|
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:
|
|
73
|
-
reason:
|
|
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:
|
|
84
|
-
reason:
|
|
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:
|
|
92
|
-
reason:
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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 =
|
|
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 [
|
|
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
|
+
}
|