@triflux/core 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 (145) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +191 -31
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +262 -34
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +1051 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +22 -21
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/schema/delegator-tools.schema.json +1 -1
  35. package/hub/delegator/service.mjs +93 -64
  36. package/hub/delegator/tool-definitions.mjs +35 -35
  37. package/hub/dynamic-routing-engine.mjs +511 -0
  38. package/hub/fullcycle.mjs +34 -18
  39. package/hub/gemini-adapter.mjs +82 -108
  40. package/hub/hitl.mjs +202 -143
  41. package/hub/intent.mjs +167 -38
  42. package/hub/lib/bash-path.mjs +73 -0
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/env-detect.mjs +145 -0
  45. package/hub/lib/hosts-compat.mjs +319 -0
  46. package/hub/lib/mcp-response-cache.mjs +239 -0
  47. package/hub/lib/memory-store.mjs +285 -111
  48. package/hub/lib/path-utils.mjs +165 -0
  49. package/hub/lib/phase-manager.mjs +493 -0
  50. package/hub/lib/process-utils.mjs +797 -104
  51. package/hub/lib/prompt-tmp.mjs +33 -0
  52. package/hub/lib/risk-tier.mjs +53 -0
  53. package/hub/lib/spawn-trace.mjs +561 -0
  54. package/hub/lib/ssh-command.mjs +44 -24
  55. package/hub/lib/ssh-retry.mjs +5 -1
  56. package/hub/lib/staged-file-detect.mjs +43 -0
  57. package/hub/lib/state-snapshot.mjs +293 -0
  58. package/hub/lib/tfx-route-args.mjs +309 -0
  59. package/hub/lib/timeline-adapter.mjs +190 -0
  60. package/hub/lib/trace-recorder.mjs +153 -0
  61. package/hub/lib/uuidv7.mjs +4 -3
  62. package/hub/memory-doctor.mjs +266 -106
  63. package/hub/middleware/quota-middleware.mjs +70 -0
  64. package/hub/middleware/request-logger.mjs +61 -34
  65. package/hub/paths.mjs +9 -9
  66. package/hub/pipeline/gates/confidence.mjs +34 -15
  67. package/hub/pipeline/gates/consensus.mjs +27 -15
  68. package/hub/pipeline/gates/index.mjs +7 -3
  69. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  70. package/hub/pipeline/index.mjs +77 -42
  71. package/hub/pipeline/state.mjs +10 -10
  72. package/hub/pipeline/transitions.mjs +40 -23
  73. package/hub/platform.mjs +65 -48
  74. package/hub/quality/deslop.mjs +70 -49
  75. package/hub/reflexion.mjs +87 -13
  76. package/hub/research.mjs +33 -25
  77. package/hub/router.mjs +941 -791
  78. package/hub/routing/complexity.mjs +132 -29
  79. package/hub/routing/index.mjs +17 -12
  80. package/hub/routing/q-learning.mjs +78 -30
  81. package/hub/routing-snapshot.mjs +261 -0
  82. package/hub/session-fingerprint.mjs +127 -61
  83. package/hub/state.mjs +86 -44
  84. package/hub/team/claude-daemon-control.mjs +1130 -0
  85. package/hub/team/retry-state-machine.mjs +336 -0
  86. package/hub/token-mode.mjs +72 -71
  87. package/hub/workers/worker-utils.mjs +23 -17
  88. package/hud/colors.mjs +27 -9
  89. package/hud/constants.mjs +190 -27
  90. package/hud/context-monitor.mjs +133 -44
  91. package/hud/hud-qos-status.mjs +157 -50
  92. package/hud/mission-board.mjs +56 -0
  93. package/hud/providers/claude.mjs +398 -114
  94. package/hud/providers/codex.mjs +96 -21
  95. package/hud/providers/gemini.mjs +210 -52
  96. package/hud/renderers.mjs +440 -127
  97. package/hud/terminal.mjs +101 -31
  98. package/hud/utils.mjs +81 -39
  99. package/mesh/index.mjs +69 -0
  100. package/mesh/mesh-budget.mjs +137 -0
  101. package/mesh/mesh-heartbeat.mjs +100 -0
  102. package/mesh/mesh-protocol.mjs +96 -0
  103. package/mesh/mesh-queue.mjs +163 -0
  104. package/mesh/mesh-registry.mjs +78 -0
  105. package/mesh/mesh-router.mjs +77 -0
  106. package/package.json +5 -1
  107. package/scripts/lib/agent-json.mjs +27 -0
  108. package/scripts/lib/async.mjs +174 -0
  109. package/scripts/lib/claudemd-scanner.mjs +6 -1
  110. package/scripts/lib/cli-agy.mjs +62 -0
  111. package/scripts/lib/cli-claude.mjs +78 -0
  112. package/scripts/lib/cli-codex.mjs +199 -0
  113. package/scripts/lib/cli-gemini.mjs +37 -0
  114. package/scripts/lib/codex-recovery.sh +50 -0
  115. package/scripts/lib/context.mjs +3 -3
  116. package/scripts/lib/cross-review-utils.mjs +13 -5
  117. package/scripts/lib/doctor-env-checks.mjs +121 -0
  118. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  119. package/scripts/lib/env-probe.mjs +58 -30
  120. package/scripts/lib/env.mjs +14 -0
  121. package/scripts/lib/gemini-profiles.mjs +44 -10
  122. package/scripts/lib/handoff.mjs +33 -17
  123. package/scripts/lib/hook-utils.mjs +8 -6
  124. package/scripts/lib/hub.mjs +48 -0
  125. package/scripts/lib/keyword-rules.mjs +49 -20
  126. package/scripts/lib/logger.mjs +24 -24
  127. package/scripts/lib/mcp-filter.mjs +391 -247
  128. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  129. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  130. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  131. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  132. package/scripts/lib/mcp-health.mjs +773 -0
  133. package/scripts/lib/mcp-manifest.mjs +24 -14
  134. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  135. package/scripts/lib/pid.mjs +63 -0
  136. package/scripts/lib/process-utils.mjs +26 -0
  137. package/scripts/lib/psmux-info.mjs +117 -12
  138. package/scripts/lib/quota.mjs +47 -0
  139. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  140. package/scripts/lib/skill-state.mjs +220 -0
  141. package/scripts/lib/skill-template.mjs +32 -7
  142. package/scripts/lib/team.mjs +75 -0
  143. package/scripts/lib/timeout.mjs +66 -0
  144. package/scripts/lib/tmp.mjs +26 -0
  145. package/scripts/lib/toml.mjs +113 -0
package/hub/bridge.mjs CHANGED
@@ -1,23 +1,47 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  // hub/bridge.mjs — tfx-route.sh ↔ tfx-hub 브릿지 CLI
3
4
  //
4
5
  // Named Pipe/Unix Socket 제어 채널을 우선 사용하고,
5
6
  // 연결이 없을 때만 HTTP /bridge/* 엔드포인트로 내려간다.
6
7
 
7
- import net from 'node:net';
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync, openSync } from 'node:fs';
9
- import { join } from 'node:path';
10
- import { homedir } from 'node:os';
11
- import { spawn } from 'node:child_process';
12
- import { parseArgs as nodeParseArgs } from 'node:util';
13
- import { randomUUID } from 'node:crypto';
14
- import { fileURLToPath } from 'node:url';
15
-
16
- import { getPipelineStateDbPath } from './pipeline/state.mjs';
8
+ import { spawn } from "node:child_process";
9
+ import { randomUUID } from "node:crypto";
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readFileSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import net from "node:net";
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { parseArgs as nodeParseArgs } from "node:util";
22
+
23
+ import { getPipelineStateDbPath } from "./pipeline/state.mjs";
24
+ import {
25
+ createRetryStateMachine,
26
+ loadSnapshot,
27
+ saveSnapshot,
28
+ } from "./team/retry-state-machine.mjs";
29
+
30
+ const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
31
+ const HUB_TOKEN_FILE = join(homedir(), ".claude", ".tfx-hub-token");
32
+ const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
33
+ const HUB_DEFAULT_PORT = 27888;
34
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
35
+
36
+ function formatHostForUrl(host) {
37
+ return host.includes(":") ? `[${host}]` : host;
38
+ }
17
39
 
18
- const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
19
- const HUB_TOKEN_FILE = join(homedir(), '.claude', '.tfx-hub-token');
20
- const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url));
40
+ function normalizeLoopbackHost(host) {
41
+ if (typeof host !== "string") return "127.0.0.1";
42
+ const candidate = host.trim();
43
+ return LOOPBACK_HOSTS.has(candidate) ? candidate : "127.0.0.1";
44
+ }
21
45
 
22
46
  function normalizeToken(raw) {
23
47
  if (raw == null) return null;
@@ -30,26 +54,31 @@ function readHubToken() {
30
54
  const envToken = normalizeToken(process.env.TFX_HUB_TOKEN);
31
55
  if (envToken) return envToken;
32
56
  try {
33
- return normalizeToken(readFileSync(HUB_TOKEN_FILE, 'utf8'));
57
+ return normalizeToken(readFileSync(HUB_TOKEN_FILE, "utf8"));
34
58
  } catch {
35
59
  return null;
36
60
  }
37
61
  }
38
62
 
39
63
  export function getHubUrl() {
40
- if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
64
+ if (process.env.TFX_HUB_URL)
65
+ return process.env.TFX_HUB_URL.replace(/\/mcp$/, "");
66
+
67
+ const envPort = Number.parseInt(String(process.env.TFX_HUB_PORT ?? ""), 10);
68
+ const port =
69
+ Number.isFinite(envPort) && envPort > 0 ? envPort : HUB_DEFAULT_PORT;
70
+ let host = "127.0.0.1";
41
71
 
42
72
  if (existsSync(HUB_PID_FILE)) {
43
73
  try {
44
- const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
45
- return `http://${info.host || '127.0.0.1'}:${info.port || 27888}`;
74
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
75
+ host = normalizeLoopbackHost(info?.host);
46
76
  } catch {
47
77
  // 무시
48
78
  }
49
79
  }
50
80
 
51
- const port = process.env.TFX_HUB_PORT || '27888';
52
- return `http://127.0.0.1:${port}`;
81
+ return `http://${formatHostForUrl(host)}:${port}`;
53
82
  }
54
83
 
55
84
  export function getHubPipePath() {
@@ -57,7 +86,7 @@ export function getHubPipePath() {
57
86
 
58
87
  if (!existsSync(HUB_PID_FILE)) return null;
59
88
  try {
60
- const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
89
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
61
90
  return info.pipe_path || info.pipePath || null;
62
91
  } catch {
63
92
  return null;
@@ -65,30 +94,144 @@ export function getHubPipePath() {
65
94
  }
66
95
 
67
96
  const HUB_OPERATIONS = Object.freeze({
68
- register: { transport: 'command', action: 'register', httpPath: '/bridge/register' },
69
- result: { transport: 'command', action: 'result', httpPath: '/bridge/result' },
70
- control: { transport: 'command', action: 'control', httpPath: '/bridge/control' },
71
- context: { transport: 'query', action: 'drain', httpPath: '/bridge/context' },
72
- deregister: { transport: 'command', action: 'deregister', httpPath: '/bridge/deregister' },
73
- assignAsync: { transport: 'command', action: 'assign', httpPath: '/bridge/assign/async' },
74
- assignResult: { transport: 'command', action: 'assign_result', httpPath: '/bridge/assign/result' },
75
- assignStatus: { transport: 'query', action: 'assign_status', httpPath: '/bridge/assign/status' },
76
- assignRetry: { transport: 'command', action: 'assign_retry', httpPath: '/bridge/assign/retry' },
77
- teamInfo: { transport: 'query', action: 'team_info', httpPath: '/bridge/team/info' },
78
- teamTaskList: { transport: 'query', action: 'team_task_list', httpPath: '/bridge/team/task-list' },
79
- teamTaskUpdate: { transport: 'command', action: 'team_task_update', httpPath: '/bridge/team/task-update' },
80
- teamSendMessage: { transport: 'command', action: 'team_send_message', httpPath: '/bridge/team/send-message' },
81
- pipelineState: { transport: 'query', action: 'pipeline_state', httpPath: '/bridge/pipeline/state' },
82
- pipelineAdvance: { transport: 'command', action: 'pipeline_advance', httpPath: '/bridge/pipeline/advance' },
83
- pipelineInit: { transport: 'command', action: 'pipeline_init', httpPath: '/bridge/pipeline/init' },
84
- pipelineList: { transport: 'query', action: 'pipeline_list', httpPath: '/bridge/pipeline/list' },
85
- hubStatus: { transport: 'query', action: 'status', httpPath: '/status', httpMethod: 'GET' },
86
- delegatorDelegate: { transport: 'command', action: 'delegator_delegate', httpPath: '/bridge/delegator/delegate' },
87
- delegatorReply: { transport: 'command', action: 'delegator_reply', httpPath: '/bridge/delegator/reply' },
88
- delegatorStatus: { transport: 'query', action: 'delegator_status', httpPath: '/bridge/delegator/status' },
97
+ register: {
98
+ transport: "command",
99
+ action: "register",
100
+ httpPath: "/bridge/register",
101
+ },
102
+ result: {
103
+ transport: "command",
104
+ action: "result",
105
+ httpPath: "/bridge/result",
106
+ },
107
+ control: {
108
+ transport: "command",
109
+ action: "control",
110
+ httpPath: "/bridge/control",
111
+ },
112
+ handoff: {
113
+ transport: "command",
114
+ action: "handoff",
115
+ httpPath: "/bridge/handoff",
116
+ },
117
+ publish: {
118
+ transport: "command",
119
+ action: "publish",
120
+ httpPath: "/bridge/publish",
121
+ },
122
+ sendInput: {
123
+ transport: "command",
124
+ action: "send_input",
125
+ httpPath: "/bridge/send-input",
126
+ },
127
+ context: { transport: "query", action: "drain", httpPath: "/bridge/context" },
128
+ deregister: {
129
+ transport: "command",
130
+ action: "deregister",
131
+ httpPath: "/bridge/deregister",
132
+ },
133
+ assignAsync: {
134
+ transport: "command",
135
+ action: "assign",
136
+ httpPath: "/bridge/assign/async",
137
+ },
138
+ assignResult: {
139
+ transport: "command",
140
+ action: "assign_result",
141
+ httpPath: "/bridge/assign/result",
142
+ },
143
+ assignStatus: {
144
+ transport: "query",
145
+ action: "assign_status",
146
+ httpPath: "/bridge/assign/status",
147
+ },
148
+ assignRetry: {
149
+ transport: "command",
150
+ action: "assign_retry",
151
+ httpPath: "/bridge/assign/retry",
152
+ },
153
+ teamInfo: {
154
+ transport: "query",
155
+ action: "team_info",
156
+ httpPath: "/bridge/team/info",
157
+ },
158
+ teamTaskList: {
159
+ transport: "query",
160
+ action: "team_task_list",
161
+ httpPath: "/bridge/team/task-list",
162
+ },
163
+ teamTaskUpdate: {
164
+ transport: "command",
165
+ action: "team_task_update",
166
+ httpPath: "/bridge/team/task-update",
167
+ },
168
+ teamSendMessage: {
169
+ transport: "command",
170
+ action: "team_send_message",
171
+ httpPath: "/bridge/team/send-message",
172
+ },
173
+ pipelineState: {
174
+ transport: "query",
175
+ action: "pipeline_state",
176
+ httpPath: "/bridge/pipeline/state",
177
+ },
178
+ pipelineAdvance: {
179
+ transport: "command",
180
+ action: "pipeline_advance",
181
+ httpPath: "/bridge/pipeline/advance",
182
+ },
183
+ pipelineInit: {
184
+ transport: "command",
185
+ action: "pipeline_init",
186
+ httpPath: "/bridge/pipeline/init",
187
+ },
188
+ pipelineList: {
189
+ transport: "query",
190
+ action: "pipeline_list",
191
+ httpPath: "/bridge/pipeline/list",
192
+ },
193
+ hubStatus: {
194
+ transport: "query",
195
+ action: "status",
196
+ httpPath: "/status",
197
+ httpMethod: "GET",
198
+ },
199
+ delegatorDelegate: {
200
+ transport: "command",
201
+ action: "delegator_delegate",
202
+ httpPath: "/bridge/delegator/delegate",
203
+ },
204
+ delegatorReply: {
205
+ transport: "command",
206
+ action: "delegator_reply",
207
+ httpPath: "/bridge/delegator/reply",
208
+ },
209
+ delegatorStatus: {
210
+ transport: "query",
211
+ action: "delegator_status",
212
+ httpPath: "/bridge/delegator/status",
213
+ },
214
+ "hitl-request": {
215
+ transport: "command",
216
+ action: "hitl_request",
217
+ httpPath: "/bridge/hitl/request",
218
+ },
219
+ "hitl-submit": {
220
+ transport: "command",
221
+ action: "hitl_submit",
222
+ httpPath: "/bridge/hitl/submit",
223
+ },
224
+ "hitl-pending": {
225
+ transport: "query",
226
+ action: "hitl_pending",
227
+ httpPath: "/bridge/hitl/pending",
228
+ },
89
229
  });
90
230
 
91
- export async function requestJson(path, { method = 'POST', body, timeoutMs = 5000 } = {}) {
231
+ export async function requestJson(
232
+ path,
233
+ { method = "POST", body, timeoutMs = 5000 } = {},
234
+ ) {
92
235
  const controller = new AbortController();
93
236
  const timer = setTimeout(() => controller.abort(), timeoutMs);
94
237
 
@@ -99,7 +242,7 @@ export async function requestJson(path, { method = 'POST', body, timeoutMs = 500
99
242
  headers.Authorization = `Bearer ${token}`;
100
243
  }
101
244
  if (body !== undefined) {
102
- headers['Content-Type'] = 'application/json';
245
+ headers["Content-Type"] = "application/json";
103
246
  }
104
247
 
105
248
  const res = await fetch(`${getHubUrl()}${path}`, {
@@ -117,7 +260,7 @@ export async function requestJson(path, { method = 'POST', body, timeoutMs = 500
117
260
  }
118
261
 
119
262
  export async function post(path, body, timeoutMs = 5000) {
120
- return await requestJson(path, { method: 'POST', body, timeoutMs });
263
+ return await requestJson(path, { method: "POST", body, timeoutMs });
121
264
  }
122
265
 
123
266
  export async function connectPipe(timeoutMs = 1200) {
@@ -127,19 +270,23 @@ export async function connectPipe(timeoutMs = 1200) {
127
270
  return await new Promise((resolve) => {
128
271
  const socket = net.createConnection(pipePath);
129
272
  const timer = setTimeout(() => {
130
- try { socket.destroy(); } catch {}
273
+ try {
274
+ socket.destroy();
275
+ } catch {}
131
276
  resolve(null);
132
277
  }, timeoutMs);
133
278
 
134
- socket.once('connect', () => {
279
+ socket.once("connect", () => {
135
280
  clearTimeout(timer);
136
- socket.setEncoding('utf8');
281
+ socket.setEncoding("utf8");
137
282
  resolve(socket);
138
283
  });
139
284
 
140
- socket.once('error', () => {
285
+ socket.once("error", () => {
141
286
  clearTimeout(timer);
142
- try { socket.destroy(); } catch {}
287
+ try {
288
+ socket.destroy();
289
+ } catch {}
143
290
  resolve(null);
144
291
  });
145
292
  });
@@ -151,7 +298,7 @@ async function pipeRequest(type, action, payload, timeoutMs = 3000) {
151
298
 
152
299
  return await new Promise((resolve) => {
153
300
  const requestId = randomUUID();
154
- let buffer = '';
301
+ let buffer = "";
155
302
  let settled = false;
156
303
  const timer = setTimeout(() => {
157
304
  finish(null);
@@ -161,17 +308,19 @@ async function pipeRequest(type, action, payload, timeoutMs = 3000) {
161
308
  if (settled) return;
162
309
  settled = true;
163
310
  clearTimeout(timer);
164
- try { socket.end(); } catch {}
311
+ try {
312
+ socket.end();
313
+ } catch {}
165
314
  resolve(result);
166
315
  };
167
316
 
168
- socket.on('data', (chunk) => {
317
+ socket.on("data", (chunk) => {
169
318
  buffer += chunk;
170
- let newlineIndex = buffer.indexOf('\n');
319
+ let newlineIndex = buffer.indexOf("\n");
171
320
  while (newlineIndex >= 0) {
172
321
  const line = buffer.slice(0, newlineIndex).trim();
173
322
  buffer = buffer.slice(newlineIndex + 1);
174
- newlineIndex = buffer.indexOf('\n');
323
+ newlineIndex = buffer.indexOf("\n");
175
324
  if (!line) continue;
176
325
 
177
326
  let frame;
@@ -181,7 +330,8 @@ async function pipeRequest(type, action, payload, timeoutMs = 3000) {
181
330
  continue;
182
331
  }
183
332
 
184
- if (frame?.type !== 'response' || frame.request_id !== requestId) continue;
333
+ if (frame?.type !== "response" || frame.request_id !== requestId)
334
+ continue;
185
335
  finish({
186
336
  ok: frame.ok,
187
337
  error: frame.error,
@@ -191,89 +341,103 @@ async function pipeRequest(type, action, payload, timeoutMs = 3000) {
191
341
  }
192
342
  });
193
343
 
194
- socket.on('error', () => finish(null));
195
- socket.write(JSON.stringify({
196
- type,
197
- request_id: requestId,
198
- payload: { action, ...payload },
199
- }) + '\n');
344
+ socket.on("error", () => finish(null));
345
+ socket.write(
346
+ JSON.stringify({
347
+ type,
348
+ request_id: requestId,
349
+ payload: { action, ...payload },
350
+ }) + "\n",
351
+ );
200
352
  });
201
353
  }
202
354
 
203
355
  async function pipeCommand(action, payload, timeoutMs = 3000) {
204
- return await pipeRequest('command', action, payload, timeoutMs);
356
+ return await pipeRequest("command", action, payload, timeoutMs);
205
357
  }
206
358
 
207
359
  async function pipeQuery(action, payload, timeoutMs = 3000) {
208
- return await pipeRequest('query', action, payload, timeoutMs);
360
+ return await pipeRequest("query", action, payload, timeoutMs);
209
361
  }
210
362
 
211
363
  export function parseArgs(argv) {
212
- const { values } = nodeParseArgs({
364
+ const { values, positionals } = nodeParseArgs({
213
365
  args: argv,
214
366
  options: {
215
- agent: { type: 'string' },
216
- cli: { type: 'string' },
217
- timeout: { type: 'string' },
218
- topics: { type: 'string' },
219
- capabilities: { type: 'string' },
220
- file: { type: 'string' },
221
- payload: { type: 'string' },
222
- topic: { type: 'string' },
223
- trace: { type: 'string' },
224
- correlation: { type: 'string' },
225
- 'exit-code': { type: 'string' },
226
- max: { type: 'string' },
227
- out: { type: 'string' },
228
- team: { type: 'string' },
229
- 'task-id': { type: 'string' },
230
- 'job-id': { type: 'string' },
231
- owner: { type: 'string' },
232
- status: { type: 'string' },
233
- statuses: { type: 'string' },
234
- claim: { type: 'boolean' },
235
- actor: { type: 'string' },
236
- command: { type: 'string' },
237
- reason: { type: 'string' },
238
- from: { type: 'string' },
239
- to: { type: 'string' },
240
- text: { type: 'string' },
241
- task: { type: 'string' },
242
- 'supervisor-agent': { type: 'string' },
243
- 'worker-agent': { type: 'string' },
244
- priority: { type: 'string' },
245
- 'ttl-ms': { type: 'string' },
246
- 'timeout-ms': { type: 'string' },
247
- 'max-retries': { type: 'string' },
248
- attempt: { type: 'string' },
249
- result: { type: 'string' },
250
- error: { type: 'string' },
251
- metadata: { type: 'string' },
252
- 'requested-by': { type: 'string' },
253
- summary: { type: 'string' },
254
- color: { type: 'string' },
255
- limit: { type: 'string' },
256
- 'include-internal': { type: 'boolean' },
257
- subject: { type: 'string' },
258
- description: { type: 'string' },
259
- 'fix-max': { type: 'string' },
260
- 'ralph-max': { type: 'string' },
261
- 'active-form': { type: 'string' },
262
- 'add-blocks': { type: 'string' },
263
- 'add-blocked-by': { type: 'string' },
264
- 'metadata-patch': { type: 'string' },
265
- 'if-match-mtime-ms': { type: 'string' },
266
- provider: { type: 'string' },
267
- mode: { type: 'string' },
268
- prompt: { type: 'string' },
269
- reply: { type: 'string' },
270
- done: { type: 'boolean' },
271
- 'mcp-profile': { type: 'string' },
272
- 'session-key': { type: 'string' },
367
+ agent: { type: "string" },
368
+ cli: { type: "string" },
369
+ timeout: { type: "string" },
370
+ topics: { type: "string" },
371
+ capabilities: { type: "string" },
372
+ file: { type: "string" },
373
+ payload: { type: "string" },
374
+ "payload-file": { type: "string" },
375
+ topic: { type: "string" },
376
+ trace: { type: "string" },
377
+ correlation: { type: "string" },
378
+ "exit-code": { type: "string" },
379
+ max: { type: "string" },
380
+ out: { type: "string" },
381
+ team: { type: "string" },
382
+ "task-id": { type: "string" },
383
+ "job-id": { type: "string" },
384
+ owner: { type: "string" },
385
+ status: { type: "string" },
386
+ statuses: { type: "string" },
387
+ claim: { type: "boolean" },
388
+ actor: { type: "string" },
389
+ command: { type: "string" },
390
+ "session-id": { type: "string" },
391
+ reason: { type: "string" },
392
+ type: { type: "string" },
393
+ from: { type: "string" },
394
+ to: { type: "string" },
395
+ text: { type: "string" },
396
+ task: { type: "string" },
397
+ "supervisor-agent": { type: "string" },
398
+ "worker-agent": { type: "string" },
399
+ priority: { type: "string" },
400
+ "ttl-ms": { type: "string" },
401
+ "timeout-ms": { type: "string" },
402
+ "max-retries": { type: "string" },
403
+ attempt: { type: "string" },
404
+ result: { type: "string" },
405
+ error: { type: "string" },
406
+ metadata: { type: "string" },
407
+ "requested-by": { type: "string" },
408
+ summary: { type: "string" },
409
+ color: { type: "string" },
410
+ limit: { type: "string" },
411
+ "include-internal": { type: "boolean" },
412
+ subject: { type: "string" },
413
+ description: { type: "string" },
414
+ "fix-max": { type: "string" },
415
+ "ralph-max": { type: "string" },
416
+ "active-form": { type: "string" },
417
+ "add-blocks": { type: "string" },
418
+ "add-blocked-by": { type: "string" },
419
+ "metadata-patch": { type: "string" },
420
+ "if-match-mtime-ms": { type: "string" },
421
+ provider: { type: "string" },
422
+ mode: { type: "string" },
423
+ prompt: { type: "string" },
424
+ reply: { type: "string" },
425
+ done: { type: "boolean" },
426
+ "mcp-profile": { type: "string" },
427
+ "session-key": { type: "string" },
428
+ snapshot: { type: "string" },
429
+ "snapshot-file": { type: "string" },
430
+ event: { type: "string" },
431
+ "max-iterations": { type: "string" },
273
432
  },
433
+ allowPositionals: true,
274
434
  strict: false,
275
435
  });
276
- return values;
436
+ const parsed = { ...values, _: positionals };
437
+ positionals.forEach((value, index) => {
438
+ parsed[index + 1] = value;
439
+ });
440
+ return parsed;
277
441
  }
278
442
 
279
443
  export function parseJsonSafe(raw, fallback = null) {
@@ -285,29 +449,78 @@ export function parseJsonSafe(raw, fallback = null) {
285
449
  }
286
450
  }
287
451
 
452
+ export function readBridgePayload(args = {}, stdin) {
453
+ if (args["payload-file"]) {
454
+ const payloadFile = String(args["payload-file"]);
455
+ const raw =
456
+ payloadFile === "-"
457
+ ? stdin === undefined
458
+ ? readFileSync(0, "utf8")
459
+ : String(stdin)
460
+ : readFileSync(payloadFile, "utf8");
461
+ return JSON.parse(raw);
462
+ }
463
+ if (args.payload) return JSON.parse(args.payload);
464
+ return {};
465
+ }
466
+
467
+ function normalizeBridgePayload(payload) {
468
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
469
+ return {};
470
+ }
471
+ return payload;
472
+ }
473
+
474
+ function buildHandoffBody(from, to, payload) {
475
+ const normalizedPayload = normalizeBridgePayload(payload);
476
+ return {
477
+ ...normalizedPayload,
478
+ from,
479
+ to,
480
+ payload: normalizedPayload,
481
+ };
482
+ }
483
+
484
+ function buildPublishBody(from, to, type, payload) {
485
+ const normalizedPayload = normalizeBridgePayload(payload);
486
+ return {
487
+ ...normalizedPayload,
488
+ from,
489
+ to,
490
+ type,
491
+ message_type: type,
492
+ payload: normalizedPayload,
493
+ };
494
+ }
495
+
288
496
  // Hub 자동 재시작 (Pipe+HTTP 모두 실패 시 1회 시도, 최대 4초 대기)
289
497
  async function tryRestartHub() {
290
- const serverPath = join(PROJECT_ROOT, 'hub', 'server.mjs');
498
+ const serverPath = join(PROJECT_ROOT, "hub", "server.mjs");
291
499
  if (!existsSync(serverPath)) return false;
292
500
 
293
501
  if (existsSync(HUB_PID_FILE)) {
294
502
  try {
295
- const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
503
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
296
504
  if (info.pid) {
297
- try { process.kill(info.pid, 0); return false; } // still alive
298
- catch (e) { if (e.code === 'EPERM') return false; } // alive, no permission
505
+ try {
506
+ process.kill(info.pid, 0);
507
+ return false;
508
+ } catch (e) {
509
+ // still alive
510
+ if (e.code === "EPERM") return false;
511
+ } // alive, no permission
299
512
  }
300
513
  } catch {} // corrupt PID file, proceed with restart
301
514
  }
302
515
 
303
516
  try {
304
- const logDir = join(process.cwd(), '.tfx', 'logs');
517
+ const logDir = join(process.cwd(), ".tfx", "logs");
305
518
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
306
- const logFile = join(logDir, 'hub-restart.log');
307
- const logFd = openSync(logFile, 'a');
519
+ const logFile = join(logDir, "hub-restart.log");
520
+ const logFd = openSync(logFile, "a");
308
521
  const child = spawn(process.execPath, [serverPath], {
309
522
  detached: true,
310
- stdio: ['ignore', 'ignore', logFd],
523
+ stdio: ["ignore", "ignore", logFd],
311
524
  windowsHide: true,
312
525
  });
313
526
  child.unref();
@@ -323,7 +536,7 @@ async function tryRestartHub() {
323
536
  });
324
537
  if (res.ok) {
325
538
  const data = await res.json();
326
- if (data?.hub?.state === 'healthy') return true;
539
+ if (data?.hub?.state === "healthy") return true;
327
540
  }
328
541
  } catch {}
329
542
  }
@@ -331,52 +544,54 @@ async function tryRestartHub() {
331
544
  }
332
545
 
333
546
  async function requestHub(operation, body, timeoutMs = 3000, fallback = null) {
334
- const viaPipe = operation.transport === 'command'
335
- ? await pipeCommand(operation.action, body, timeoutMs)
336
- : await pipeQuery(operation.action, body, timeoutMs);
547
+ const viaPipe =
548
+ operation.transport === "command"
549
+ ? await pipeCommand(operation.action, body, timeoutMs)
550
+ : await pipeQuery(operation.action, body, timeoutMs);
337
551
  if (viaPipe) {
338
- return { transport: 'pipe', result: viaPipe };
552
+ return { transport: "pipe", result: viaPipe };
339
553
  }
340
554
 
341
555
  const viaHttp = operation.httpPath
342
556
  ? await requestJson(operation.httpPath, {
343
- method: operation.httpMethod || 'POST',
344
- body: operation.httpMethod === 'GET' ? undefined : body,
345
- timeoutMs: Math.max(timeoutMs, 5000),
346
- })
557
+ method: operation.httpMethod || "POST",
558
+ body: operation.httpMethod === "GET" ? undefined : body,
559
+ timeoutMs: Math.max(timeoutMs, 5000),
560
+ })
347
561
  : null;
348
562
  if (viaHttp) {
349
- return { transport: 'http', result: viaHttp };
563
+ return { transport: "http", result: viaHttp };
350
564
  }
351
565
 
352
566
  // Hub 재시작 시도 → Pipe/HTTP 재시도
353
567
  if (await tryRestartHub()) {
354
- const retryPipe = operation.transport === 'command'
355
- ? await pipeCommand(operation.action, body, timeoutMs)
356
- : await pipeQuery(operation.action, body, timeoutMs);
568
+ const retryPipe =
569
+ operation.transport === "command"
570
+ ? await pipeCommand(operation.action, body, timeoutMs)
571
+ : await pipeQuery(operation.action, body, timeoutMs);
357
572
  if (retryPipe) {
358
- return { transport: 'pipe', result: retryPipe };
573
+ return { transport: "pipe", result: retryPipe };
359
574
  }
360
575
  const retryHttp = operation.httpPath
361
576
  ? await requestJson(operation.httpPath, {
362
- method: operation.httpMethod || 'POST',
363
- body: operation.httpMethod === 'GET' ? undefined : body,
364
- timeoutMs: Math.max(timeoutMs, 5000),
365
- })
577
+ method: operation.httpMethod || "POST",
578
+ body: operation.httpMethod === "GET" ? undefined : body,
579
+ timeoutMs: Math.max(timeoutMs, 5000),
580
+ })
366
581
  : null;
367
582
  if (retryHttp) {
368
- return { transport: 'http', result: retryHttp };
583
+ return { transport: "http", result: retryHttp };
369
584
  }
370
585
  }
371
586
 
372
587
  if (!fallback) return null;
373
588
  const viaFallback = await fallback();
374
589
  if (!viaFallback) return null;
375
- return { transport: 'fallback', result: viaFallback };
590
+ return { transport: "fallback", result: viaFallback };
376
591
  }
377
592
 
378
593
  function unavailableResult() {
379
- return { ok: false, reason: 'hub_unavailable' };
594
+ return { ok: false, reason: "hub_unavailable" };
380
595
  }
381
596
 
382
597
  function emitJson(payload) {
@@ -388,14 +603,14 @@ function emitJson(payload) {
388
603
 
389
604
  async function cmdRegister(args) {
390
605
  const agentId = args.agent;
391
- const timeoutSec = parseInt(args.timeout || '600', 10);
606
+ const timeoutSec = parseInt(args.timeout || "600", 10);
392
607
  const outcome = await requestHub(HUB_OPERATIONS.register, {
393
608
  agent_id: agentId,
394
- cli: args.cli || 'other',
609
+ cli: args.cli || "other",
395
610
  timeout_sec: timeoutSec,
396
611
  heartbeat_ttl_ms: (timeoutSec + 120) * 1000,
397
- topics: args.topics ? args.topics.split(',') : [],
398
- capabilities: args.capabilities ? args.capabilities.split(',') : ['code'],
612
+ topics: args.topics ? args.topics.split(",") : [],
613
+ capabilities: args.capabilities ? args.capabilities.split(",") : ["code"],
399
614
  metadata: {
400
615
  pid: process.ppid,
401
616
  registered_at: Date.now(),
@@ -416,14 +631,14 @@ async function cmdRegister(args) {
416
631
  }
417
632
 
418
633
  async function cmdResult(args) {
419
- let output = '';
634
+ let output = "";
420
635
  if (args.file && existsSync(args.file)) {
421
- output = readFileSync(args.file, 'utf8').slice(0, 49152);
636
+ output = readFileSync(args.file, "utf8").slice(0, 49152);
422
637
  }
423
638
 
424
639
  const defaultPayload = {
425
640
  agent_id: args.agent,
426
- exit_code: parseInt(args['exit-code'] || '0', 10),
641
+ exit_code: parseInt(args["exit-code"] || "0", 10),
427
642
  output_length: output.length,
428
643
  output_preview: output.slice(0, 4096),
429
644
  output_file: args.file || null,
@@ -432,8 +647,10 @@ async function cmdResult(args) {
432
647
 
433
648
  const outcome = await requestHub(HUB_OPERATIONS.result, {
434
649
  agent_id: args.agent,
435
- topic: args.topic || 'task.result',
436
- payload: args.payload ? parseJsonSafe(args.payload, defaultPayload) : defaultPayload,
650
+ topic: args.topic || "task.result",
651
+ payload: args.payload
652
+ ? parseJsonSafe(args.payload, defaultPayload)
653
+ : defaultPayload,
437
654
  trace_id: args.trace || undefined,
438
655
  correlation_id: args.correlation || undefined,
439
656
  });
@@ -448,14 +665,48 @@ async function cmdResult(args) {
448
665
 
449
666
  async function cmdControl(args) {
450
667
  const outcome = await requestHub(HUB_OPERATIONS.control, {
451
- from_agent: args.from || 'lead',
668
+ from_agent: args.from || "lead",
452
669
  to_agent: args.to,
453
670
  command: args.command,
454
- reason: args.reason || '',
671
+ reason: args.reason || "",
455
672
  payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
456
673
  trace_id: args.trace || undefined,
457
674
  correlation_id: args.correlation || undefined,
458
- ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
675
+ ttl_ms: args["ttl-ms"] != null ? Number(args["ttl-ms"]) : undefined,
676
+ });
677
+ const result = outcome?.result;
678
+ return emitJson(result || unavailableResult());
679
+ }
680
+
681
+ async function cmdHandoff(args) {
682
+ const from = args.from || args[1];
683
+ const to = args.to || args[2];
684
+ const payload = parseJsonSafe(args.payload || args[3] || "{}", {});
685
+ const outcome = await requestHub(
686
+ HUB_OPERATIONS.handoff,
687
+ buildHandoffBody(from, to, payload),
688
+ );
689
+ const result = outcome?.result;
690
+ return emitJson(result || unavailableResult());
691
+ }
692
+
693
+ async function cmdPublish(args) {
694
+ const from = args.from || args[1];
695
+ const to = args.to || args[2];
696
+ const type = args.type || args[3] || "event";
697
+ const payload = parseJsonSafe(args.payload || args[4] || "{}", {});
698
+ const outcome = await requestHub(
699
+ HUB_OPERATIONS.publish,
700
+ buildPublishBody(from, to, type, payload),
701
+ );
702
+ const result = outcome?.result;
703
+ return emitJson(result || unavailableResult());
704
+ }
705
+
706
+ async function cmdSendInput(args) {
707
+ const outcome = await requestHub(HUB_OPERATIONS.sendInput, {
708
+ session_id: args["session-id"],
709
+ text: args.text,
459
710
  });
460
711
  const result = outcome?.result;
461
712
  return emitJson(result || unavailableResult());
@@ -464,26 +715,35 @@ async function cmdControl(args) {
464
715
  async function cmdContext(args) {
465
716
  const outcome = await requestHub(HUB_OPERATIONS.context, {
466
717
  agent_id: args.agent,
467
- topics: args.topics ? args.topics.split(',') : undefined,
468
- max_messages: parseInt(args.max || '10', 10),
718
+ topics: args.topics ? args.topics.split(",") : undefined,
719
+ max_messages: parseInt(args.max || "10", 10),
469
720
  auto_ack: true,
470
721
  });
471
722
  const result = outcome?.result;
472
723
 
473
724
  if (result?.ok && result.data?.messages?.length) {
474
725
  const parts = result.data.messages.map((message, index) => {
475
- const payload = typeof message.payload === 'string'
476
- ? message.payload
477
- : JSON.stringify(message.payload, null, 2);
478
- return `=== Context ${index + 1}: ${message.from_agent || 'unknown'} (${message.topic || 'unknown'}) ===\n${payload}`;
726
+ const payload =
727
+ typeof message.payload === "string"
728
+ ? message.payload
729
+ : JSON.stringify(message.payload, null, 2);
730
+ return `=== Context ${index + 1}: ${message.from_agent || "unknown"} (${message.topic || "unknown"}) ===\n${payload}`;
479
731
  });
480
- const combined = parts.join('\n\n');
732
+ const combined = parts.join("\n\n");
481
733
 
482
734
  if (args.out) {
483
- writeFileSync(args.out, combined, 'utf8');
484
- return emitJson({ ok: true, count: result.data.messages.length, file: args.out });
735
+ writeFileSync(args.out, combined, "utf8");
736
+ return emitJson({
737
+ ok: true,
738
+ count: result.data.messages.length,
739
+ file: args.out,
740
+ });
485
741
  } else {
486
- return emitJson({ ok: true, count: result.data.messages.length, context: combined });
742
+ return emitJson({
743
+ ok: true,
744
+ count: result.data.messages.length,
745
+ context: combined,
746
+ });
487
747
  }
488
748
  }
489
749
 
@@ -491,7 +751,7 @@ async function cmdContext(args) {
491
751
  if (args.out) {
492
752
  return emitJson({ ok: true, count: 0 });
493
753
  }
494
- return emitJson({ ok: true, count: 0, context: '' });
754
+ return emitJson({ ok: true, count: 0, context: "" });
495
755
  }
496
756
 
497
757
  return emitJson(result || unavailableResult());
@@ -504,7 +764,7 @@ async function cmdDeregister(args) {
504
764
  const result = outcome?.result;
505
765
 
506
766
  if (result?.ok) {
507
- return emitJson({ ok: true, agent_id: args.agent, status: 'offline' });
767
+ return emitJson({ ok: true, agent_id: args.agent, status: "offline" });
508
768
  }
509
769
 
510
770
  return emitJson(result || unavailableResult());
@@ -512,15 +772,17 @@ async function cmdDeregister(args) {
512
772
 
513
773
  async function cmdAssignAsync(args) {
514
774
  const outcome = await requestHub(HUB_OPERATIONS.assignAsync, {
515
- supervisor_agent: args['supervisor-agent'],
516
- worker_agent: args['worker-agent'],
775
+ supervisor_agent: args["supervisor-agent"],
776
+ worker_agent: args["worker-agent"],
517
777
  task: args.task,
518
- topic: args.topic || 'assign.job',
778
+ topic: args.topic || "assign.job",
519
779
  payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
520
780
  priority: args.priority != null ? Number(args.priority) : undefined,
521
- ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
522
- timeout_ms: args['timeout-ms'] != null ? Number(args['timeout-ms']) : undefined,
523
- max_retries: args['max-retries'] != null ? Number(args['max-retries']) : undefined,
781
+ ttl_ms: args["ttl-ms"] != null ? Number(args["ttl-ms"]) : undefined,
782
+ timeout_ms:
783
+ args["timeout-ms"] != null ? Number(args["timeout-ms"]) : undefined,
784
+ max_retries:
785
+ args["max-retries"] != null ? Number(args["max-retries"]) : undefined,
524
786
  trace_id: args.trace || undefined,
525
787
  correlation_id: args.correlation || undefined,
526
788
  });
@@ -530,8 +792,8 @@ async function cmdAssignAsync(args) {
530
792
 
531
793
  async function cmdAssignResult(args) {
532
794
  const outcome = await requestHub(HUB_OPERATIONS.assignResult, {
533
- job_id: args['job-id'],
534
- worker_agent: args['worker-agent'],
795
+ job_id: args["job-id"],
796
+ worker_agent: args["worker-agent"],
535
797
  status: args.status,
536
798
  attempt: args.attempt != null ? Number(args.attempt) : undefined,
537
799
  result: args.result ? parseJsonSafe(args.result, null) : undefined,
@@ -545,7 +807,7 @@ async function cmdAssignResult(args) {
545
807
 
546
808
  async function cmdAssignStatus(args) {
547
809
  const outcome = await requestHub(HUB_OPERATIONS.assignStatus, {
548
- job_id: args['job-id'],
810
+ job_id: args["job-id"],
549
811
  });
550
812
  const result = outcome?.result;
551
813
  return emitJson(result || unavailableResult());
@@ -553,9 +815,9 @@ async function cmdAssignStatus(args) {
553
815
 
554
816
  async function cmdAssignRetry(args) {
555
817
  const outcome = await requestHub(HUB_OPERATIONS.assignRetry, {
556
- job_id: args['job-id'],
818
+ job_id: args["job-id"],
557
819
  reason: args.reason,
558
- requested_by: args['requested-by'],
820
+ requested_by: args["requested-by"],
559
821
  });
560
822
  const result = outcome?.result;
561
823
  return emitJson(result || unavailableResult());
@@ -567,10 +829,15 @@ async function cmdTeamInfo(args) {
567
829
  include_members: true,
568
830
  include_paths: true,
569
831
  };
570
- const outcome = await requestHub(HUB_OPERATIONS.teamInfo, body, 3000, async () => {
571
- const { teamInfo } = await import('./team/nativeProxy.mjs');
572
- return await teamInfo(body);
573
- });
832
+ const outcome = await requestHub(
833
+ HUB_OPERATIONS.teamInfo,
834
+ body,
835
+ 3000,
836
+ async () => {
837
+ const { teamInfo } = await import("./team/nativeProxy.mjs");
838
+ return await teamInfo(body);
839
+ },
840
+ );
574
841
  const result = outcome?.result;
575
842
  return emitJson(result || unavailableResult());
576
843
  }
@@ -579,14 +846,24 @@ async function cmdTeamTaskList(args) {
579
846
  const body = {
580
847
  team_name: args.team,
581
848
  owner: args.owner,
582
- statuses: args.statuses ? args.statuses.split(',').map((status) => status.trim()).filter(Boolean) : [],
583
- include_internal: !!args['include-internal'],
584
- limit: parseInt(args.limit || '200', 10),
849
+ statuses: args.statuses
850
+ ? args.statuses
851
+ .split(",")
852
+ .map((status) => status.trim())
853
+ .filter(Boolean)
854
+ : [],
855
+ include_internal: !!args["include-internal"],
856
+ limit: parseInt(args.limit || "200", 10),
585
857
  };
586
- const outcome = await requestHub(HUB_OPERATIONS.teamTaskList, body, 3000, async () => {
587
- const { teamTaskList } = await import('./team/nativeProxy.mjs');
588
- return await teamTaskList(body);
589
- });
858
+ const outcome = await requestHub(
859
+ HUB_OPERATIONS.teamTaskList,
860
+ body,
861
+ 3000,
862
+ async () => {
863
+ const { teamTaskList } = await import("./team/nativeProxy.mjs");
864
+ return await teamTaskList(body);
865
+ },
866
+ );
590
867
  const result = outcome?.result;
591
868
  return emitJson(result || unavailableResult());
592
869
  }
@@ -594,23 +871,43 @@ async function cmdTeamTaskList(args) {
594
871
  async function cmdTeamTaskUpdate(args) {
595
872
  const body = {
596
873
  team_name: args.team,
597
- task_id: args['task-id'],
874
+ task_id: args["task-id"],
598
875
  claim: !!args.claim,
599
876
  owner: args.owner,
600
877
  status: args.status,
601
878
  subject: args.subject,
602
879
  description: args.description,
603
- activeForm: args['active-form'],
604
- add_blocks: args['add-blocks'] ? args['add-blocks'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
605
- add_blocked_by: args['add-blocked-by'] ? args['add-blocked-by'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
606
- metadata_patch: args['metadata-patch'] ? parseJsonSafe(args['metadata-patch'], null) : undefined,
607
- if_match_mtime_ms: args['if-match-mtime-ms'] != null ? Number(args['if-match-mtime-ms']) : undefined,
880
+ activeForm: args["active-form"],
881
+ add_blocks: args["add-blocks"]
882
+ ? args["add-blocks"]
883
+ .split(",")
884
+ .map((value) => value.trim())
885
+ .filter(Boolean)
886
+ : undefined,
887
+ add_blocked_by: args["add-blocked-by"]
888
+ ? args["add-blocked-by"]
889
+ .split(",")
890
+ .map((value) => value.trim())
891
+ .filter(Boolean)
892
+ : undefined,
893
+ metadata_patch: args["metadata-patch"]
894
+ ? parseJsonSafe(args["metadata-patch"], null)
895
+ : undefined,
896
+ if_match_mtime_ms:
897
+ args["if-match-mtime-ms"] != null
898
+ ? Number(args["if-match-mtime-ms"])
899
+ : undefined,
608
900
  actor: args.actor,
609
901
  };
610
- const outcome = await requestHub(HUB_OPERATIONS.teamTaskUpdate, body, 3000, async () => {
611
- const { teamTaskUpdate } = await import('./team/nativeProxy.mjs');
612
- return await teamTaskUpdate(body);
613
- });
902
+ const outcome = await requestHub(
903
+ HUB_OPERATIONS.teamTaskUpdate,
904
+ body,
905
+ 3000,
906
+ async () => {
907
+ const { teamTaskUpdate } = await import("./team/nativeProxy.mjs");
908
+ return await teamTaskUpdate(body);
909
+ },
910
+ );
614
911
  const result = outcome?.result;
615
912
  return emitJson(result || unavailableResult());
616
913
  }
@@ -619,15 +916,20 @@ async function cmdTeamSendMessage(args) {
619
916
  const body = {
620
917
  team_name: args.team,
621
918
  from: args.from,
622
- to: args.to || 'team-lead',
919
+ to: args.to || "team-lead",
623
920
  text: args.text,
624
921
  summary: args.summary,
625
- color: args.color || 'blue',
922
+ color: args.color || "blue",
626
923
  };
627
- const outcome = await requestHub(HUB_OPERATIONS.teamSendMessage, body, 3000, async () => {
628
- const { teamSendMessage } = await import('./team/nativeProxy.mjs');
629
- return await teamSendMessage(body);
630
- });
924
+ const outcome = await requestHub(
925
+ HUB_OPERATIONS.teamSendMessage,
926
+ body,
927
+ 3000,
928
+ async () => {
929
+ const { teamSendMessage } = await import("./team/nativeProxy.mjs");
930
+ return await teamSendMessage(body);
931
+ },
932
+ );
631
933
  const result = outcome?.result;
632
934
  return emitJson(result || unavailableResult());
633
935
  }
@@ -637,62 +939,122 @@ function getHubDbPath() {
637
939
  }
638
940
 
639
941
  async function cmdPipelineState(args) {
640
- const outcome = await requestHub(HUB_OPERATIONS.pipelineState, { team_name: args.team }, 3000, async () => {
641
- try {
642
- const { default: Database } = await import('better-sqlite3');
643
- const { ensurePipelineTable, readPipelineState } = await import('./pipeline/state.mjs');
644
- const dbPath = getHubDbPath();
645
- if (!existsSync(dbPath)) {
646
- return { ok: false, error: 'hub_db_not_found' };
942
+ const outcome = await requestHub(
943
+ HUB_OPERATIONS.pipelineState,
944
+ { team_name: args.team },
945
+ 3000,
946
+ async () => {
947
+ try {
948
+ const { default: Database } = await import("better-sqlite3");
949
+ const { ensurePipelineTable, readPipelineState } = await import(
950
+ "./pipeline/state.mjs"
951
+ );
952
+ const dbPath = getHubDbPath();
953
+ if (!existsSync(dbPath)) {
954
+ return { ok: false, error: "hub_db_not_found" };
955
+ }
956
+ const db = new Database(dbPath, { readonly: true });
957
+ ensurePipelineTable(db);
958
+ const state = readPipelineState(db, args.team);
959
+ db.close();
960
+ return state
961
+ ? { ok: true, data: state }
962
+ : { ok: false, error: "pipeline_not_found" };
963
+ } catch (e) {
964
+ return { ok: false, error: e.message };
647
965
  }
648
- const db = new Database(dbPath, { readonly: true });
649
- ensurePipelineTable(db);
650
- const state = readPipelineState(db, args.team);
651
- db.close();
652
- return state
653
- ? { ok: true, data: state }
654
- : { ok: false, error: 'pipeline_not_found' };
655
- } catch (e) {
656
- return { ok: false, error: e.message };
657
- }
658
- });
966
+ },
967
+ );
659
968
  const result = outcome?.result;
660
969
  return emitJson(result || unavailableResult());
661
970
  }
662
971
 
972
+ // Best-effort phase-manager bridge (silent on failure — pipeline 동작 영향 X)
973
+ async function writePhaseSafely(runId, phase, status) {
974
+ if (!runId) return;
975
+ try {
976
+ const mod = await import("./lib/phase-manager.mjs");
977
+ if (typeof mod?.writePhase === "function") {
978
+ await mod.writePhase(runId, phase, status);
979
+ }
980
+ if (typeof mod?.syncToGstack === "function") {
981
+ const slug = process.cwd().split(/[\\/]/).pop();
982
+ await mod.syncToGstack(runId, slug);
983
+ }
984
+ } catch {
985
+ /* silent — phase tracking is best-effort */
986
+ }
987
+ }
988
+
989
+ async function writePhaseSafelyFromLegacy(runId, legacyStatus) {
990
+ if (!runId || !legacyStatus) return;
991
+ try {
992
+ const mod = await import("./lib/phase-manager.mjs");
993
+ if (typeof mod?.coerceLegacyPhase !== "function") return;
994
+ const coerced = mod.coerceLegacyPhase(legacyStatus);
995
+ if (!coerced) return;
996
+ if (coerced === "complete") {
997
+ const current =
998
+ typeof mod.readPhase === "function" ? await mod.readPhase(runId) : null;
999
+ const lastPhase = current?.phase || current?.lastPhase;
1000
+ if (lastPhase) await mod.writePhase(runId, lastPhase, "complete");
1001
+ return;
1002
+ }
1003
+ await mod.writePhase(runId, coerced, "active");
1004
+ if (typeof mod?.syncToGstack === "function") {
1005
+ const slug = process.cwd().split(/[\\/]/).pop();
1006
+ await mod.syncToGstack(runId, slug);
1007
+ }
1008
+ } catch {
1009
+ /* silent */
1010
+ }
1011
+ }
1012
+
663
1013
  async function cmdPipelineAdvance(args) {
664
1014
  const body = {
665
1015
  team_name: args.team,
666
1016
  phase: args.status, // --status를 phase로 재활용
667
1017
  };
668
- const outcome = await requestHub(HUB_OPERATIONS.pipelineAdvance, body, 3000, async () => {
669
- try {
670
- const { default: Database } = await import('better-sqlite3');
671
- const { createPipeline } = await import('./pipeline/index.mjs');
672
- const dbPath = getHubDbPath();
673
- if (!existsSync(dbPath)) {
674
- return { ok: false, error: 'hub_db_not_found' };
1018
+ const outcome = await requestHub(
1019
+ HUB_OPERATIONS.pipelineAdvance,
1020
+ body,
1021
+ 3000,
1022
+ async () => {
1023
+ try {
1024
+ const { default: Database } = await import("better-sqlite3");
1025
+ const { createPipeline } = await import("./pipeline/index.mjs");
1026
+ const dbPath = getHubDbPath();
1027
+ if (!existsSync(dbPath)) {
1028
+ return { ok: false, error: "hub_db_not_found" };
1029
+ }
1030
+ const db = new Database(dbPath);
1031
+ const pipeline = createPipeline(db, args.team);
1032
+ const advanceResult = pipeline.advance(args.status);
1033
+ db.close();
1034
+ return advanceResult;
1035
+ } catch (e) {
1036
+ return { ok: false, error: e.message };
675
1037
  }
676
- const db = new Database(dbPath);
677
- const pipeline = createPipeline(db, args.team);
678
- const advanceResult = pipeline.advance(args.status);
679
- db.close();
680
- return advanceResult;
681
- } catch (e) {
682
- return { ok: false, error: e.message };
683
- }
684
- });
1038
+ },
1039
+ );
685
1040
  const result = outcome?.result;
1041
+ if (args.team && args.status && result?.ok !== false) {
1042
+ void writePhaseSafelyFromLegacy(args.team, args.status);
1043
+ }
686
1044
  return emitJson(result || unavailableResult());
687
1045
  }
688
1046
 
689
1047
  async function cmdPipelineInit(args) {
690
1048
  const outcome = await requestHub(HUB_OPERATIONS.pipelineInit, {
691
1049
  team_name: args.team,
692
- fix_max: args['fix-max'] != null ? Number(args['fix-max']) : undefined,
693
- ralph_max: args['ralph-max'] != null ? Number(args['ralph-max']) : undefined,
1050
+ fix_max: args["fix-max"] != null ? Number(args["fix-max"]) : undefined,
1051
+ ralph_max:
1052
+ args["ralph-max"] != null ? Number(args["ralph-max"]) : undefined,
694
1053
  });
695
1054
  const result = outcome?.result;
1055
+ if (args.team && result?.ok !== false) {
1056
+ void writePhaseSafely(args.team, "Strategy", "active");
1057
+ }
696
1058
  return emitJson(result || unavailableResult());
697
1059
  }
698
1060
 
@@ -703,25 +1065,29 @@ async function cmdPipelineList() {
703
1065
  }
704
1066
 
705
1067
  async function cmdPing() {
706
- const outcome = await requestHub(HUB_OPERATIONS.hubStatus, { scope: 'hub' }, 2000);
1068
+ const outcome = await requestHub(
1069
+ HUB_OPERATIONS.hubStatus,
1070
+ { scope: "hub" },
1071
+ 2000,
1072
+ );
707
1073
 
708
- if (outcome?.transport === 'pipe' && outcome.result?.ok) {
1074
+ if (outcome?.transport === "pipe" && outcome.result?.ok) {
709
1075
  return emitJson({
710
1076
  ok: true,
711
- hub: outcome.result.data?.hub?.state || 'healthy',
1077
+ hub: outcome.result.data?.hub?.state || "healthy",
712
1078
  pipe_path: getHubPipePath(),
713
- transport: 'pipe',
1079
+ transport: "pipe",
714
1080
  });
715
1081
  }
716
1082
 
717
- if (outcome?.transport === 'http' && outcome.result) {
1083
+ if (outcome?.transport === "http" && outcome.result) {
718
1084
  const data = outcome.result;
719
1085
  return emitJson({
720
1086
  ok: true,
721
1087
  hub: data.hub?.state,
722
1088
  sessions: data.sessions,
723
1089
  pipe_path: data.pipe?.path || data.pipe_path || null,
724
- transport: 'http',
1090
+ transport: "http",
725
1091
  });
726
1092
  }
727
1093
 
@@ -731,21 +1097,26 @@ async function cmdPing() {
731
1097
  async function cmdDelegatorDelegate(args) {
732
1098
  const body = {
733
1099
  prompt: args.text || args.prompt,
734
- provider: args.provider || 'auto',
735
- mode: args.mode || 'sync',
736
- agent_type: args.agent || 'executor',
737
- mcp_profile: args['mcp-profile'] || 'auto',
738
- session_key: args['session-key'] || undefined,
739
- timeout_ms: args['timeout-ms'] != null ? Number(args['timeout-ms']) : undefined,
1100
+ provider: args.provider || "auto",
1101
+ mode: args.mode || "sync",
1102
+ agent_type: args.agent || "executor",
1103
+ mcp_profile: args["mcp-profile"] || "auto",
1104
+ session_key: args["session-key"] || undefined,
1105
+ timeout_ms:
1106
+ args["timeout-ms"] != null ? Number(args["timeout-ms"]) : undefined,
740
1107
  };
741
- const timeoutMs = body.mode === 'async' ? 10000 : 120000;
742
- const outcome = await requestHub(HUB_OPERATIONS.delegatorDelegate, body, timeoutMs);
1108
+ const timeoutMs = body.mode === "async" ? 10000 : 120000;
1109
+ const outcome = await requestHub(
1110
+ HUB_OPERATIONS.delegatorDelegate,
1111
+ body,
1112
+ timeoutMs,
1113
+ );
743
1114
  return emitJson(outcome?.result || unavailableResult());
744
1115
  }
745
1116
 
746
1117
  async function cmdDelegatorReply(args) {
747
1118
  const body = {
748
- job_id: args['job-id'],
1119
+ job_id: args["job-id"],
749
1120
  reply: args.text || args.reply,
750
1121
  done: !!args.done,
751
1122
  };
@@ -755,45 +1126,435 @@ async function cmdDelegatorReply(args) {
755
1126
 
756
1127
  async function cmdDelegatorStatus(args) {
757
1128
  const body = {
758
- job_id: args['job-id'],
1129
+ job_id: args["job-id"],
759
1130
  };
760
1131
  const outcome = await requestHub(HUB_OPERATIONS.delegatorStatus, body, 5000);
761
1132
  return emitJson(outcome?.result || unavailableResult());
762
1133
  }
763
1134
 
1135
+ async function cmdHitlRequest(args) {
1136
+ const body = {
1137
+ kind: args[1],
1138
+ prompt: args[2],
1139
+ requester_agent: args[3] || "cli",
1140
+ };
1141
+ const outcome = await requestHub(
1142
+ HUB_OPERATIONS["hitl-request"],
1143
+ body,
1144
+ 120000,
1145
+ );
1146
+ return emitJson(outcome?.result || unavailableResult());
1147
+ }
1148
+
1149
+ async function cmdHitlSubmit(args) {
1150
+ const body = {
1151
+ request_id: args[1],
1152
+ action: args[2] || "accept",
1153
+ content: args[3],
1154
+ };
1155
+ const outcome = await requestHub(HUB_OPERATIONS["hitl-submit"], body, 120000);
1156
+ return emitJson(outcome?.result || unavailableResult());
1157
+ }
1158
+
1159
+ async function cmdHitlPending() {
1160
+ const outcome = await requestHub(HUB_OPERATIONS["hitl-pending"], {}, 5000);
1161
+ return emitJson(outcome?.result || unavailableResult());
1162
+ }
1163
+
1164
+ async function loadDaemonControl() {
1165
+ return await import("./team/claude-daemon-control.mjs");
1166
+ }
1167
+
1168
+ function daemonErrorResult(error) {
1169
+ return {
1170
+ ok: false,
1171
+ error: error?.message || String(error),
1172
+ };
1173
+ }
1174
+
1175
+ function daemonAttachErrorResult(error) {
1176
+ return {
1177
+ ok: false,
1178
+ text: "",
1179
+ raw: "",
1180
+ responseRaw: "",
1181
+ matchedCompletion: false,
1182
+ timedOut: false,
1183
+ closed: false,
1184
+ inputSent: error?.inputSent === true,
1185
+ error: error?.message || String(error),
1186
+ };
1187
+ }
1188
+
1189
+ function daemonInterruptErrorResult(error) {
1190
+ return {
1191
+ ok: false,
1192
+ done: false,
1193
+ aborted: false,
1194
+ reason: "interrupt_failed",
1195
+ inputSent: error?.inputSent === true,
1196
+ error: error?.message || String(error),
1197
+ };
1198
+ }
1199
+
1200
+ function numericOption(value, fallback) {
1201
+ const parsed = Number(value);
1202
+ return Number.isFinite(parsed) ? parsed : fallback;
1203
+ }
1204
+
1205
+ async function cmdDaemonProbe(args) {
1206
+ try {
1207
+ const payload = readBridgePayload(args);
1208
+ const {
1209
+ deriveClaudeDaemonPaths,
1210
+ findDaemonJobBySessionId,
1211
+ findDaemonJobByShort,
1212
+ sendClaudeControlRequest,
1213
+ } = await loadDaemonControl();
1214
+ const daemonPaths = deriveClaudeDaemonPaths({
1215
+ configDir: payload.configDir,
1216
+ });
1217
+ const list = await sendClaudeControlRequest(
1218
+ daemonPaths.controlSock,
1219
+ { proto: 1, op: "list" },
1220
+ { timeoutMs: numericOption(payload.timeoutMs, 6000) },
1221
+ );
1222
+ const target = payload.sessionId
1223
+ ? findDaemonJobBySessionId(list, payload.sessionId)
1224
+ : payload.short
1225
+ ? findDaemonJobByShort(list, payload.short)
1226
+ : null;
1227
+
1228
+ return emitJson({
1229
+ ok: list?.ok !== false,
1230
+ controlSock: daemonPaths.controlSock,
1231
+ sessions: list?.jobs || list?.sessions || [],
1232
+ target: target || undefined,
1233
+ error: list?.ok === false ? list?.error : undefined,
1234
+ });
1235
+ } catch (error) {
1236
+ return emitJson(daemonErrorResult(error));
1237
+ }
1238
+ }
1239
+
1240
+ async function cmdDaemonAttach(args) {
1241
+ try {
1242
+ const payload = readBridgePayload(args);
1243
+ if (!payload.prompt) throw new Error("prompt is required");
1244
+
1245
+ const {
1246
+ attachClaudeDaemonSession,
1247
+ deriveClaudeDaemonPaths,
1248
+ findDaemonJobBySessionId,
1249
+ sendClaudeControlRequest,
1250
+ } = await loadDaemonControl();
1251
+ const daemonPaths = deriveClaudeDaemonPaths({
1252
+ configDir: payload.configDir,
1253
+ });
1254
+ let short = payload.short;
1255
+ if (!short && payload.sessionId) {
1256
+ const list = await sendClaudeControlRequest(
1257
+ daemonPaths.controlSock,
1258
+ { proto: 1, op: "list" },
1259
+ { timeoutMs: numericOption(payload.timeoutMs, 6000) },
1260
+ );
1261
+ const target = findDaemonJobBySessionId(list, payload.sessionId);
1262
+ short = target?.short;
1263
+ }
1264
+ if (!short) throw new Error("short or resolvable sessionId is required");
1265
+
1266
+ const result = await attachClaudeDaemonSession({
1267
+ controlSock: daemonPaths.controlSock,
1268
+ short,
1269
+ input: payload.prompt,
1270
+ cols: numericOption(payload.cols, undefined),
1271
+ rows: numericOption(payload.rows, undefined),
1272
+ timeoutMs: numericOption(payload.timeoutMs, 30_000),
1273
+ });
1274
+
1275
+ return emitJson({
1276
+ ok: result.matchedCompletion === true,
1277
+ text: result.text,
1278
+ raw: result.streamText,
1279
+ responseRaw: result.responseStreamText,
1280
+ matchedCompletion: result.matchedCompletion === true,
1281
+ timedOut: result.timedOut === true,
1282
+ closed: result.closed === true,
1283
+ inputSent: result.inputSent === true,
1284
+ error:
1285
+ result.handshake?.ok === false ? result.handshake?.error : undefined,
1286
+ });
1287
+ } catch (error) {
1288
+ return emitJson(daemonAttachErrorResult(error));
1289
+ }
1290
+ }
1291
+
1292
+ async function cmdDaemonInterrupt(args) {
1293
+ try {
1294
+ const payload = readBridgePayload(args);
1295
+
1296
+ const {
1297
+ deriveClaudeDaemonPaths,
1298
+ findDaemonJobBySessionId,
1299
+ interruptClaudeDaemonSession,
1300
+ sendClaudeControlRequest,
1301
+ } = await loadDaemonControl();
1302
+ const daemonPaths = deriveClaudeDaemonPaths({
1303
+ configDir: payload.configDir,
1304
+ });
1305
+ let short = payload.short;
1306
+ if (!short && payload.sessionId) {
1307
+ const list = await sendClaudeControlRequest(
1308
+ daemonPaths.controlSock,
1309
+ { proto: 1, op: "list" },
1310
+ { timeoutMs: numericOption(payload.timeoutMs, 6000) },
1311
+ );
1312
+ const target = findDaemonJobBySessionId(list, payload.sessionId);
1313
+ short = target?.short;
1314
+ }
1315
+ if (!short) throw new Error("short or resolvable sessionId is required");
1316
+
1317
+ const result = await interruptClaudeDaemonSession({
1318
+ controlSock: daemonPaths.controlSock,
1319
+ short,
1320
+ cols: numericOption(payload.cols, undefined),
1321
+ rows: numericOption(payload.rows, undefined),
1322
+ timeoutMs: numericOption(payload.timeoutMs, 5000),
1323
+ });
1324
+ const aborted = result.inputSent === true;
1325
+
1326
+ return emitJson({
1327
+ ok: aborted,
1328
+ done: false,
1329
+ aborted,
1330
+ reason: aborted ? "user_interrupt" : "interrupt_failed",
1331
+ raw: result.streamText,
1332
+ timedOut: result.timedOut === true,
1333
+ closed: result.closed === true,
1334
+ inputSent: result.inputSent === true,
1335
+ error:
1336
+ result.handshake?.ok === false ? result.handshake?.error : undefined,
1337
+ });
1338
+ } catch (error) {
1339
+ return emitJson(daemonInterruptErrorResult(error));
1340
+ }
1341
+ }
1342
+
1343
+ // ---------------------------------------------------------------------------
1344
+ // retry-run / retry-status — Phase 3 Step C2 bridge 서브커맨드.
1345
+ // retry-state-machine.mjs 를 multi-process safe 하게 외부 호출용 wrap.
1346
+ // 사용자 워크플로우:
1347
+ // 1) 첫 호출: retry-run --snapshot X --mode ralph --event start
1348
+ // → 새 SM 생성, PLANNING → EXECUTING transition, snapshot 저장
1349
+ // 2) verify 성공 시: retry-run --snapshot X --event verify-success
1350
+ // → DONE, 종료 판단 반환
1351
+ // 3) verify 실패 시: retry-run --snapshot X --event verify-fail --reason R
1352
+ // → DIAGNOSING 또는 STUCK/BUDGET_EXCEEDED, 종료 판단 반환
1353
+ // 4) 다음 iter 시작: retry-run --snapshot X --event start
1354
+ // 출력: {ok, current, iterations, done, shouldStop, reason?, cli?} JSON.
1355
+ // ---------------------------------------------------------------------------
1356
+
1357
+ function buildRetrySmFromArgs(args, snapshot) {
1358
+ const mode = args.mode || snapshot?.mode || "bounded";
1359
+ const maxIterations =
1360
+ args["max-iterations"] !== undefined
1361
+ ? Number(args["max-iterations"])
1362
+ : snapshot?.maxIterations;
1363
+ const sessionId = args["session-id"] || snapshot?.sessionId || null;
1364
+ const cliChain = snapshot?.cliChain;
1365
+
1366
+ const sm = createRetryStateMachine({
1367
+ mode,
1368
+ maxIterations,
1369
+ sessionId,
1370
+ cliChain,
1371
+ });
1372
+ if (snapshot) sm.applySnapshot(snapshot);
1373
+ return sm;
1374
+ }
1375
+
1376
+ function buildRetryCliInvocation(cli) {
1377
+ if (!cli) return null;
1378
+
1379
+ const invocation = {
1380
+ cli: cli.cli,
1381
+ model: cli.model,
1382
+ argv: [],
1383
+ };
1384
+ if (cli.profile) invocation.profile = cli.profile;
1385
+ if (cli.cli === "codex" && cli.profile) {
1386
+ invocation.argv.push("--profile", cli.profile);
1387
+ }
1388
+ return invocation;
1389
+ }
1390
+
1391
+ async function cmdRetryRun(args) {
1392
+ const snapshotFile = args.snapshot || args["snapshot-file"];
1393
+ const event = args.event;
1394
+ const reason = args.reason || "";
1395
+
1396
+ if (!snapshotFile) {
1397
+ console.error("--snapshot <path> required");
1398
+ return false;
1399
+ }
1400
+ if (!event) {
1401
+ console.error("--event <start|verify-success|verify-fail> required");
1402
+ return false;
1403
+ }
1404
+
1405
+ const existing = loadSnapshot(snapshotFile);
1406
+ const sm = buildRetrySmFromArgs(args, existing);
1407
+
1408
+ let result;
1409
+ switch (event) {
1410
+ case "start":
1411
+ result = sm.startIteration();
1412
+ break;
1413
+ case "verify-success":
1414
+ result = sm.reportVerifySuccess();
1415
+ break;
1416
+ case "verify-fail":
1417
+ result = sm.reportVerifyFail(reason || "unspecified");
1418
+ break;
1419
+ default:
1420
+ console.error(`unknown --event: ${event}`);
1421
+ return false;
1422
+ }
1423
+
1424
+ const snap = sm.serialize();
1425
+ saveSnapshot(snapshotFile, snap);
1426
+
1427
+ const terminal = ["DONE", "STUCK", "BUDGET_EXCEEDED"].includes(snap.current);
1428
+ const cli = snap.cliChain?.[snap.cliIndex] || null;
1429
+ const cliInvocation = buildRetryCliInvocation(cli);
1430
+ const out = {
1431
+ ok: true,
1432
+ current: snap.current,
1433
+ iterations: snap.iterations,
1434
+ cliIndex: snap.cliIndex,
1435
+ cli,
1436
+ cliInvocation,
1437
+ done: snap.current === "DONE",
1438
+ shouldStop: terminal,
1439
+ stuckCounter: snap.stuckCounter,
1440
+ lastFailureReason: snap.lastFailureReason,
1441
+ transition: result,
1442
+ };
1443
+ console.log(JSON.stringify(out));
1444
+ return true;
1445
+ }
1446
+
1447
+ async function cmdRetryStatus(args) {
1448
+ const snapshotFile = args.snapshot || args["snapshot-file"];
1449
+ if (!snapshotFile) {
1450
+ console.error("--snapshot <path> required");
1451
+ return false;
1452
+ }
1453
+ const snap = loadSnapshot(snapshotFile);
1454
+ if (!snap) {
1455
+ console.log(JSON.stringify({ ok: true, exists: false }));
1456
+ return true;
1457
+ }
1458
+ const terminal = ["DONE", "STUCK", "BUDGET_EXCEEDED"].includes(snap.current);
1459
+ const cli = snap.cliChain?.[snap.cliIndex] || null;
1460
+ const cliInvocation = buildRetryCliInvocation(cli);
1461
+ console.log(
1462
+ JSON.stringify({
1463
+ ok: true,
1464
+ exists: true,
1465
+ current: snap.current,
1466
+ iterations: snap.iterations,
1467
+ maxIterations: snap.maxIterations,
1468
+ cliIndex: snap.cliIndex,
1469
+ cli,
1470
+ cliInvocation,
1471
+ mode: snap.mode,
1472
+ shouldStop: terminal,
1473
+ stuckCounter: snap.stuckCounter,
1474
+ lastFailureReason: snap.lastFailureReason,
1475
+ }),
1476
+ );
1477
+ return true;
1478
+ }
1479
+
764
1480
  export async function main(argv = process.argv.slice(2)) {
765
1481
  const cmd = argv[0];
766
1482
  const args = parseArgs(argv.slice(1));
767
1483
 
768
1484
  switch (cmd) {
769
- case 'register': return await cmdRegister(args);
770
- case 'result': return await cmdResult(args);
771
- case 'control': return await cmdControl(args);
772
- case 'context': return await cmdContext(args);
773
- case 'deregister': return await cmdDeregister(args);
774
- case 'assign-async': return await cmdAssignAsync(args);
775
- case 'assign-result': return await cmdAssignResult(args);
776
- case 'assign-status': return await cmdAssignStatus(args);
777
- case 'assign-retry': return await cmdAssignRetry(args);
778
- case 'team-info': return await cmdTeamInfo(args);
779
- case 'team-task-list': return await cmdTeamTaskList(args);
780
- case 'team-task-update': return await cmdTeamTaskUpdate(args);
781
- case 'team-send-message': return await cmdTeamSendMessage(args);
782
- case 'pipeline-state': return await cmdPipelineState(args);
783
- case 'pipeline-advance': return await cmdPipelineAdvance(args);
784
- case 'pipeline-init': return await cmdPipelineInit(args);
785
- case 'pipeline-list': return await cmdPipelineList(args);
786
- case 'ping': return await cmdPing(args);
787
- case 'delegator-delegate': return await cmdDelegatorDelegate(args);
788
- case 'delegator-reply': return await cmdDelegatorReply(args);
789
- case 'delegator-status': return await cmdDelegatorStatus(args);
1485
+ case "register":
1486
+ return await cmdRegister(args);
1487
+ case "result":
1488
+ return await cmdResult(args);
1489
+ case "control":
1490
+ return await cmdControl(args);
1491
+ case "handoff":
1492
+ return await cmdHandoff(args);
1493
+ case "publish":
1494
+ return await cmdPublish(args);
1495
+ case "send-input":
1496
+ return await cmdSendInput(args);
1497
+ case "context":
1498
+ return await cmdContext(args);
1499
+ case "deregister":
1500
+ return await cmdDeregister(args);
1501
+ case "assign-async":
1502
+ return await cmdAssignAsync(args);
1503
+ case "assign-result":
1504
+ return await cmdAssignResult(args);
1505
+ case "assign-status":
1506
+ return await cmdAssignStatus(args);
1507
+ case "assign-retry":
1508
+ return await cmdAssignRetry(args);
1509
+ case "team-info":
1510
+ return await cmdTeamInfo(args);
1511
+ case "team-task-list":
1512
+ return await cmdTeamTaskList(args);
1513
+ case "team-task-update":
1514
+ return await cmdTeamTaskUpdate(args);
1515
+ case "team-send-message":
1516
+ return await cmdTeamSendMessage(args);
1517
+ case "pipeline-state":
1518
+ return await cmdPipelineState(args);
1519
+ case "pipeline-advance":
1520
+ return await cmdPipelineAdvance(args);
1521
+ case "pipeline-init":
1522
+ return await cmdPipelineInit(args);
1523
+ case "pipeline-list":
1524
+ return await cmdPipelineList(args);
1525
+ case "ping":
1526
+ return await cmdPing(args);
1527
+ case "delegator-delegate":
1528
+ return await cmdDelegatorDelegate(args);
1529
+ case "delegator-reply":
1530
+ return await cmdDelegatorReply(args);
1531
+ case "delegator-status":
1532
+ return await cmdDelegatorStatus(args);
1533
+ case "hitl-request":
1534
+ return await cmdHitlRequest(args);
1535
+ case "hitl-submit":
1536
+ return await cmdHitlSubmit(args);
1537
+ case "hitl-pending":
1538
+ return await cmdHitlPending(args);
1539
+ case "daemon-probe":
1540
+ return await cmdDaemonProbe(args);
1541
+ case "daemon-attach":
1542
+ return await cmdDaemonAttach(args);
1543
+ case "daemon-interrupt":
1544
+ return await cmdDaemonInterrupt(args);
1545
+ case "retry-run":
1546
+ return await cmdRetryRun(args);
1547
+ case "retry-status":
1548
+ return await cmdRetryStatus(args);
790
1549
  default:
791
- console.error('사용법: bridge.mjs <register|result|control|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status> [--옵션]');
1550
+ console.error(
1551
+ "사용법: bridge.mjs <register|result|control|handoff|publish|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|daemon-probe|daemon-attach|daemon-interrupt|retry-run|retry-status> [--옵션]",
1552
+ );
792
1553
  process.exit(1);
793
1554
  }
794
1555
  }
795
1556
 
796
- const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/bridge.mjs');
1557
+ const selfRun = process.argv[1]?.replace(/\\/g, "/").endsWith("hub/bridge.mjs");
797
1558
  if (selfRun) {
798
- process.exitCode = await main() ? 0 : 1;
1559
+ process.exitCode = (await main()) ? 0 : 1;
799
1560
  }