triflux 3.3.0-dev.7 → 4.0.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 (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2415 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -708
  8. package/hub/delegator/contracts.mjs +38 -0
  9. package/hub/delegator/index.mjs +14 -0
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -0
  11. package/hub/delegator/service.mjs +302 -0
  12. package/hub/delegator/tool-definitions.mjs +35 -0
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -266
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +61 -60
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
package/hub/bridge.mjs CHANGED
@@ -1,708 +1,734 @@
1
- #!/usr/bin/env node
2
- // hub/bridge.mjs — tfx-route.sh ↔ tfx-hub 브릿지 CLI
3
- //
4
- // Named Pipe/Unix Socket 제어 채널을 우선 사용하고,
5
- // 연결이 없을 때만 HTTP /bridge/* 엔드포인트로 내려간다.
6
-
7
- import net from 'node:net';
8
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
9
- import { join } from 'node:path';
10
- import { homedir } from 'node:os';
11
- import { parseArgs as nodeParseArgs } from 'node:util';
12
- import { randomUUID } from 'node:crypto';
13
-
14
- const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
15
- const HUB_TOKEN_FILE = join(homedir(), '.claude', '.tfx-hub-token');
16
-
17
- function normalizeToken(raw) {
18
- if (raw == null) return null;
19
- const token = String(raw).trim();
20
- return token || null;
21
- }
22
-
23
- // Hub 인증 토큰 읽기 (파일 없으면 null → 하위 호환)
24
- function readHubToken() {
25
- const envToken = normalizeToken(process.env.TFX_HUB_TOKEN);
26
- if (envToken) return envToken;
27
- try {
28
- return normalizeToken(readFileSync(HUB_TOKEN_FILE, 'utf8'));
29
- } catch {
30
- return null;
31
- }
32
- }
33
-
34
- export function getHubUrl() {
35
- if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
36
-
37
- if (existsSync(HUB_PID_FILE)) {
38
- try {
39
- const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
40
- return `http://${info.host || '127.0.0.1'}:${info.port || 27888}`;
41
- } catch {
42
- // 무시
43
- }
44
- }
45
-
46
- const port = process.env.TFX_HUB_PORT || '27888';
47
- return `http://127.0.0.1:${port}`;
48
- }
49
-
50
- export function getHubPipePath() {
51
- if (process.env.TFX_HUB_PIPE) return process.env.TFX_HUB_PIPE;
52
-
53
- if (!existsSync(HUB_PID_FILE)) return null;
54
- try {
55
- const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
56
- return info.pipe_path || info.pipePath || null;
57
- } catch {
58
- return null;
59
- }
60
- }
61
-
62
- const HUB_OPERATIONS = Object.freeze({
63
- register: { transport: 'command', action: 'register', httpPath: '/bridge/register' },
64
- result: { transport: 'command', action: 'result', httpPath: '/bridge/result' },
65
- control: { transport: 'command', action: 'control', httpPath: '/bridge/control' },
66
- context: { transport: 'query', action: 'drain', httpPath: '/bridge/context' },
67
- deregister: { transport: 'command', action: 'deregister', httpPath: '/bridge/deregister' },
68
- assignAsync: { transport: 'command', action: 'assign', httpPath: '/bridge/assign/async' },
69
- assignResult: { transport: 'command', action: 'assign_result', httpPath: '/bridge/assign/result' },
70
- assignStatus: { transport: 'query', action: 'assign_status', httpPath: '/bridge/assign/status' },
71
- assignRetry: { transport: 'command', action: 'assign_retry', httpPath: '/bridge/assign/retry' },
72
- teamInfo: { transport: 'query', action: 'team_info', httpPath: '/bridge/team/info' },
73
- teamTaskList: { transport: 'query', action: 'team_task_list', httpPath: '/bridge/team/task-list' },
74
- teamTaskUpdate: { transport: 'command', action: 'team_task_update', httpPath: '/bridge/team/task-update' },
75
- teamSendMessage: { transport: 'command', action: 'team_send_message', httpPath: '/bridge/team/send-message' },
76
- pipelineState: { transport: 'query', action: 'pipeline_state', httpPath: '/bridge/pipeline/state' },
77
- pipelineAdvance: { transport: 'command', action: 'pipeline_advance', httpPath: '/bridge/pipeline/advance' },
78
- pipelineInit: { transport: 'command', action: 'pipeline_init', httpPath: '/bridge/pipeline/init' },
79
- pipelineList: { transport: 'query', action: 'pipeline_list', httpPath: '/bridge/pipeline/list' },
80
- hubStatus: { transport: 'query', action: 'status', httpPath: '/status', httpMethod: 'GET' },
81
- });
82
-
83
- export async function requestJson(path, { method = 'POST', body, timeoutMs = 5000 } = {}) {
84
- const controller = new AbortController();
85
- const timer = setTimeout(() => controller.abort(), timeoutMs);
86
-
87
- try {
88
- const headers = {};
89
- const token = readHubToken();
90
- if (token) {
91
- headers['Authorization'] = `Bearer ${token}`;
92
- }
93
- if (body !== undefined) {
94
- headers['Content-Type'] = 'application/json';
95
- }
96
-
97
- const res = await fetch(`${getHubUrl()}${path}`, {
98
- method,
99
- headers,
100
- body: body === undefined ? undefined : JSON.stringify(body),
101
- signal: controller.signal,
102
- });
103
- clearTimeout(timer);
104
- return await res.json();
105
- } catch {
106
- clearTimeout(timer);
107
- return null;
108
- }
109
- }
110
-
111
- export async function post(path, body, timeoutMs = 5000) {
112
- return await requestJson(path, { method: 'POST', body, timeoutMs });
113
- }
114
-
115
- export async function connectPipe(timeoutMs = 1200) {
116
- const pipePath = getHubPipePath();
117
- if (!pipePath) return null;
118
-
119
- return await new Promise((resolve) => {
120
- const socket = net.createConnection(pipePath);
121
- const timer = setTimeout(() => {
122
- try { socket.destroy(); } catch {}
123
- resolve(null);
124
- }, timeoutMs);
125
-
126
- socket.once('connect', () => {
127
- clearTimeout(timer);
128
- socket.setEncoding('utf8');
129
- resolve(socket);
130
- });
131
-
132
- socket.once('error', () => {
133
- clearTimeout(timer);
134
- try { socket.destroy(); } catch {}
135
- resolve(null);
136
- });
137
- });
138
- }
139
-
140
- async function pipeRequest(type, action, payload, timeoutMs = 3000) {
141
- const socket = await connectPipe(Math.min(timeoutMs, 1500));
142
- if (!socket) return null;
143
-
144
- return await new Promise((resolve) => {
145
- const requestId = randomUUID();
146
- let buffer = '';
147
- let settled = false;
148
- const timer = setTimeout(() => {
149
- finish(null);
150
- }, timeoutMs);
151
-
152
- const finish = (result) => {
153
- if (settled) return;
154
- settled = true;
155
- clearTimeout(timer);
156
- try { socket.end(); } catch {}
157
- resolve(result);
158
- };
159
-
160
- socket.on('data', (chunk) => {
161
- buffer += chunk;
162
- let newlineIndex = buffer.indexOf('\n');
163
- while (newlineIndex >= 0) {
164
- const line = buffer.slice(0, newlineIndex).trim();
165
- buffer = buffer.slice(newlineIndex + 1);
166
- newlineIndex = buffer.indexOf('\n');
167
- if (!line) continue;
168
-
169
- let frame;
170
- try {
171
- frame = JSON.parse(line);
172
- } catch {
173
- continue;
174
- }
175
-
176
- if (frame?.type !== 'response' || frame.request_id !== requestId) continue;
177
- finish({
178
- ok: frame.ok,
179
- error: frame.error,
180
- data: frame.data,
181
- });
182
- return;
183
- }
184
- });
185
-
186
- socket.on('error', () => finish(null));
187
- socket.write(JSON.stringify({
188
- type,
189
- request_id: requestId,
190
- payload: { action, ...payload },
191
- }) + '\n');
192
- });
193
- }
194
-
195
- async function pipeCommand(action, payload, timeoutMs = 3000) {
196
- return await pipeRequest('command', action, payload, timeoutMs);
197
- }
198
-
199
- async function pipeQuery(action, payload, timeoutMs = 3000) {
200
- return await pipeRequest('query', action, payload, timeoutMs);
201
- }
202
-
203
- export function parseArgs(argv) {
204
- const { values } = nodeParseArgs({
205
- args: argv,
206
- options: {
207
- agent: { type: 'string' },
208
- cli: { type: 'string' },
209
- timeout: { type: 'string' },
210
- topics: { type: 'string' },
211
- capabilities: { type: 'string' },
212
- file: { type: 'string' },
213
- payload: { type: 'string' },
214
- topic: { type: 'string' },
215
- trace: { type: 'string' },
216
- correlation: { type: 'string' },
217
- 'exit-code': { type: 'string' },
218
- max: { type: 'string' },
219
- out: { type: 'string' },
220
- team: { type: 'string' },
221
- 'task-id': { type: 'string' },
222
- 'job-id': { type: 'string' },
223
- owner: { type: 'string' },
224
- status: { type: 'string' },
225
- statuses: { type: 'string' },
226
- claim: { type: 'boolean' },
227
- actor: { type: 'string' },
228
- command: { type: 'string' },
229
- reason: { type: 'string' },
230
- from: { type: 'string' },
231
- to: { type: 'string' },
232
- text: { type: 'string' },
233
- task: { type: 'string' },
234
- 'supervisor-agent': { type: 'string' },
235
- 'worker-agent': { type: 'string' },
236
- priority: { type: 'string' },
237
- 'ttl-ms': { type: 'string' },
238
- 'timeout-ms': { type: 'string' },
239
- 'max-retries': { type: 'string' },
240
- attempt: { type: 'string' },
241
- result: { type: 'string' },
242
- error: { type: 'string' },
243
- metadata: { type: 'string' },
244
- 'requested-by': { type: 'string' },
245
- summary: { type: 'string' },
246
- color: { type: 'string' },
247
- limit: { type: 'string' },
248
- 'include-internal': { type: 'boolean' },
249
- subject: { type: 'string' },
250
- description: { type: 'string' },
251
- 'fix-max': { type: 'string' },
252
- 'ralph-max': { type: 'string' },
253
- 'active-form': { type: 'string' },
254
- 'add-blocks': { type: 'string' },
255
- 'add-blocked-by': { type: 'string' },
256
- 'metadata-patch': { type: 'string' },
257
- 'if-match-mtime-ms': { type: 'string' },
258
- },
259
- strict: false,
260
- });
261
- return values;
262
- }
263
-
264
- export function parseJsonSafe(raw, fallback = null) {
265
- if (!raw) return fallback;
266
- try {
267
- return JSON.parse(raw);
268
- } catch {
269
- return fallback;
270
- }
271
- }
272
-
273
- async function requestHub(operation, body, timeoutMs = 3000, fallback = null) {
274
- const viaPipe = operation.transport === 'command'
275
- ? await pipeCommand(operation.action, body, timeoutMs)
276
- : await pipeQuery(operation.action, body, timeoutMs);
277
- if (viaPipe) {
278
- return { transport: 'pipe', result: viaPipe };
279
- }
280
-
281
- const viaHttp = operation.httpPath
282
- ? await requestJson(operation.httpPath, {
283
- method: operation.httpMethod || 'POST',
284
- body: operation.httpMethod === 'GET' ? undefined : body,
285
- timeoutMs: Math.max(timeoutMs, 5000),
286
- })
287
- : null;
288
- if (viaHttp) {
289
- return { transport: 'http', result: viaHttp };
290
- }
291
-
292
- if (!fallback) return null;
293
- const viaFallback = await fallback();
294
- if (!viaFallback) return null;
295
- return { transport: 'fallback', result: viaFallback };
296
- }
297
-
298
- async function cmdRegister(args) {
299
- const agentId = args.agent;
300
- const timeoutSec = parseInt(args.timeout || '600', 10);
301
- const outcome = await requestHub(HUB_OPERATIONS.register, {
302
- agent_id: agentId,
303
- cli: args.cli || 'other',
304
- timeout_sec: timeoutSec,
305
- heartbeat_ttl_ms: (timeoutSec + 120) * 1000,
306
- topics: args.topics ? args.topics.split(',') : [],
307
- capabilities: args.capabilities ? args.capabilities.split(',') : ['code'],
308
- metadata: {
309
- pid: process.ppid,
310
- registered_at: Date.now(),
311
- },
312
- });
313
- const result = outcome?.result;
314
-
315
- if (result?.ok) {
316
- console.log(JSON.stringify({
317
- ok: true,
318
- agent_id: agentId,
319
- lease_expires_ms: result.data?.lease_expires_ms,
320
- pipe_path: result.data?.pipe_path || getHubPipePath(),
321
- }));
322
- } else {
323
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
324
- }
325
- }
326
-
327
- async function cmdResult(args) {
328
- let output = '';
329
- if (args.file && existsSync(args.file)) {
330
- output = readFileSync(args.file, 'utf8').slice(0, 49152);
331
- }
332
-
333
- const defaultPayload = {
334
- agent_id: args.agent,
335
- exit_code: parseInt(args['exit-code'] || '0', 10),
336
- output_length: output.length,
337
- output_preview: output.slice(0, 4096),
338
- output_file: args.file || null,
339
- completed_at: Date.now(),
340
- };
341
-
342
- const outcome = await requestHub(HUB_OPERATIONS.result, {
343
- agent_id: args.agent,
344
- topic: args.topic || 'task.result',
345
- payload: args.payload ? parseJsonSafe(args.payload, defaultPayload) : defaultPayload,
346
- trace_id: args.trace || undefined,
347
- correlation_id: args.correlation || undefined,
348
- });
349
- const result = outcome?.result;
350
-
351
- if (result?.ok) {
352
- console.log(JSON.stringify({ ok: true, message_id: result.data?.message_id }));
353
- } else {
354
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
355
- }
356
- }
357
-
358
- async function cmdControl(args) {
359
- const outcome = await requestHub(HUB_OPERATIONS.control, {
360
- from_agent: args.from || 'lead',
361
- to_agent: args.to,
362
- command: args.command,
363
- reason: args.reason || '',
364
- payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
365
- trace_id: args.trace || undefined,
366
- correlation_id: args.correlation || undefined,
367
- ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
368
- });
369
- const result = outcome?.result;
370
- if (result) {
371
- console.log(JSON.stringify(result));
372
- } else {
373
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
374
- }
375
- }
376
-
377
- async function cmdContext(args) {
378
- const outcome = await requestHub(HUB_OPERATIONS.context, {
379
- agent_id: args.agent,
380
- topics: args.topics ? args.topics.split(',') : undefined,
381
- max_messages: parseInt(args.max || '10', 10),
382
- auto_ack: true,
383
- });
384
- const result = outcome?.result;
385
-
386
- if (result?.ok && result.data?.messages?.length) {
387
- const parts = result.data.messages.map((message, index) => {
388
- const payload = typeof message.payload === 'string'
389
- ? message.payload
390
- : JSON.stringify(message.payload, null, 2);
391
- return `=== Context ${index + 1}: ${message.from_agent || 'unknown'} (${message.topic || 'unknown'}) ===\n${payload}`;
392
- });
393
- const combined = parts.join('\n\n');
394
-
395
- if (args.out) {
396
- writeFileSync(args.out, combined, 'utf8');
397
- console.log(JSON.stringify({ ok: true, count: result.data.messages.length, file: args.out }));
398
- } else {
399
- console.log(combined);
400
- }
401
- return;
402
- }
403
-
404
- if (args.out) console.log(JSON.stringify({ ok: true, count: 0 }));
405
- }
406
-
407
- async function cmdDeregister(args) {
408
- const outcome = await requestHub(HUB_OPERATIONS.deregister, {
409
- agent_id: args.agent,
410
- });
411
- const result = outcome?.result;
412
-
413
- if (result?.ok) {
414
- console.log(JSON.stringify({ ok: true, agent_id: args.agent, status: 'offline' }));
415
- } else {
416
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
417
- }
418
- }
419
-
420
- async function cmdAssignAsync(args) {
421
- const outcome = await requestHub(HUB_OPERATIONS.assignAsync, {
422
- supervisor_agent: args['supervisor-agent'],
423
- worker_agent: args['worker-agent'],
424
- task: args.task,
425
- topic: args.topic || 'assign.job',
426
- payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
427
- priority: args.priority != null ? Number(args.priority) : undefined,
428
- ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
429
- timeout_ms: args['timeout-ms'] != null ? Number(args['timeout-ms']) : undefined,
430
- max_retries: args['max-retries'] != null ? Number(args['max-retries']) : undefined,
431
- trace_id: args.trace || undefined,
432
- correlation_id: args.correlation || undefined,
433
- });
434
- const result = outcome?.result;
435
- if (result) {
436
- console.log(JSON.stringify(result));
437
- } else {
438
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
439
- }
440
- }
441
-
442
- async function cmdAssignResult(args) {
443
- const outcome = await requestHub(HUB_OPERATIONS.assignResult, {
444
- job_id: args['job-id'],
445
- worker_agent: args['worker-agent'],
446
- status: args.status,
447
- attempt: args.attempt != null ? Number(args.attempt) : undefined,
448
- result: args.result ? parseJsonSafe(args.result, null) : undefined,
449
- error: args.error ? parseJsonSafe(args.error, null) : undefined,
450
- payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
451
- metadata: args.metadata ? parseJsonSafe(args.metadata, {}) : {},
452
- });
453
- const result = outcome?.result;
454
- if (result) {
455
- console.log(JSON.stringify(result));
456
- } else {
457
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
458
- }
459
- }
460
-
461
- async function cmdAssignStatus(args) {
462
- const outcome = await requestHub(HUB_OPERATIONS.assignStatus, {
463
- job_id: args['job-id'],
464
- });
465
- const result = outcome?.result;
466
- if (result) {
467
- console.log(JSON.stringify(result));
468
- } else {
469
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
470
- }
471
- }
472
-
473
- async function cmdAssignRetry(args) {
474
- const outcome = await requestHub(HUB_OPERATIONS.assignRetry, {
475
- job_id: args['job-id'],
476
- reason: args.reason,
477
- requested_by: args['requested-by'],
478
- });
479
- const result = outcome?.result;
480
- if (result) {
481
- console.log(JSON.stringify(result));
482
- } else {
483
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
484
- }
485
- }
486
-
487
- async function cmdTeamInfo(args) {
488
- const body = {
489
- team_name: args.team,
490
- include_members: true,
491
- include_paths: true,
492
- };
493
- const outcome = await requestHub(HUB_OPERATIONS.teamInfo, body, 3000, async () => {
494
- const { teamInfo } = await import('./team/nativeProxy.mjs');
495
- return await teamInfo(body);
496
- });
497
- const result = outcome?.result;
498
- if (result) {
499
- console.log(JSON.stringify(result));
500
- }
501
- }
502
-
503
- async function cmdTeamTaskList(args) {
504
- const body = {
505
- team_name: args.team,
506
- owner: args.owner,
507
- statuses: args.statuses ? args.statuses.split(',').map((status) => status.trim()).filter(Boolean) : [],
508
- include_internal: !!args['include-internal'],
509
- limit: parseInt(args.limit || '200', 10),
510
- };
511
- const outcome = await requestHub(HUB_OPERATIONS.teamTaskList, body, 3000, async () => {
512
- const { teamTaskList } = await import('./team/nativeProxy.mjs');
513
- return await teamTaskList(body);
514
- });
515
- const result = outcome?.result;
516
- if (result) {
517
- console.log(JSON.stringify(result));
518
- }
519
- }
520
-
521
- async function cmdTeamTaskUpdate(args) {
522
- const body = {
523
- team_name: args.team,
524
- task_id: args['task-id'],
525
- claim: !!args.claim,
526
- owner: args.owner,
527
- status: args.status,
528
- subject: args.subject,
529
- description: args.description,
530
- activeForm: args['active-form'],
531
- add_blocks: args['add-blocks'] ? args['add-blocks'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
532
- add_blocked_by: args['add-blocked-by'] ? args['add-blocked-by'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
533
- metadata_patch: args['metadata-patch'] ? parseJsonSafe(args['metadata-patch'], null) : undefined,
534
- if_match_mtime_ms: args['if-match-mtime-ms'] != null ? Number(args['if-match-mtime-ms']) : undefined,
535
- actor: args.actor,
536
- };
537
- const outcome = await requestHub(HUB_OPERATIONS.teamTaskUpdate, body, 3000, async () => {
538
- const { teamTaskUpdate } = await import('./team/nativeProxy.mjs');
539
- return await teamTaskUpdate(body);
540
- });
541
- const result = outcome?.result;
542
- if (result) {
543
- console.log(JSON.stringify(result));
544
- }
545
- }
546
-
547
- async function cmdTeamSendMessage(args) {
548
- const body = {
549
- team_name: args.team,
550
- from: args.from,
551
- to: args.to || 'team-lead',
552
- text: args.text,
553
- summary: args.summary,
554
- color: args.color || 'blue',
555
- };
556
- const outcome = await requestHub(HUB_OPERATIONS.teamSendMessage, body, 3000, async () => {
557
- const { teamSendMessage } = await import('./team/nativeProxy.mjs');
558
- return await teamSendMessage(body);
559
- });
560
- const result = outcome?.result;
561
- if (result) {
562
- console.log(JSON.stringify(result));
563
- }
564
- }
565
-
566
- function getHubDbPath() {
567
- return join(homedir(), '.claude', 'cache', 'tfx-hub', 'state.db');
568
- }
569
-
570
- async function cmdPipelineState(args) {
571
- const outcome = await requestHub(HUB_OPERATIONS.pipelineState, { team_name: args.team }, 3000, async () => {
572
- try {
573
- const { default: Database } = await import('better-sqlite3');
574
- const { ensurePipelineTable, readPipelineState } = await import('./pipeline/state.mjs');
575
- const dbPath = getHubDbPath();
576
- if (!existsSync(dbPath)) {
577
- return { ok: false, error: 'hub_db_not_found' };
578
- }
579
- const db = new Database(dbPath, { readonly: true });
580
- ensurePipelineTable(db);
581
- const state = readPipelineState(db, args.team);
582
- db.close();
583
- return state
584
- ? { ok: true, data: state }
585
- : { ok: false, error: 'pipeline_not_found' };
586
- } catch (e) {
587
- return { ok: false, error: e.message };
588
- }
589
- });
590
- const result = outcome?.result;
591
- if (result) {
592
- console.log(JSON.stringify(result));
593
- }
594
- }
595
-
596
- async function cmdPipelineAdvance(args) {
597
- const body = {
598
- team_name: args.team,
599
- phase: args.status, // --status를 phase로 재활용
600
- };
601
- const outcome = await requestHub(HUB_OPERATIONS.pipelineAdvance, body, 3000, async () => {
602
- try {
603
- const { default: Database } = await import('better-sqlite3');
604
- const { createPipeline } = await import('./pipeline/index.mjs');
605
- const dbPath = getHubDbPath();
606
- if (!existsSync(dbPath)) {
607
- return { ok: false, error: 'hub_db_not_found' };
608
- }
609
- const db = new Database(dbPath);
610
- const pipeline = createPipeline(db, args.team);
611
- const advanceResult = pipeline.advance(args.status);
612
- db.close();
613
- return advanceResult;
614
- } catch (e) {
615
- return { ok: false, error: e.message };
616
- }
617
- });
618
- const result = outcome?.result;
619
- if (result) {
620
- console.log(JSON.stringify(result));
621
- }
622
- }
623
-
624
- async function cmdPipelineInit(args) {
625
- const outcome = await requestHub(HUB_OPERATIONS.pipelineInit, {
626
- team_name: args.team,
627
- fix_max: args['fix-max'] != null ? Number(args['fix-max']) : undefined,
628
- ralph_max: args['ralph-max'] != null ? Number(args['ralph-max']) : undefined,
629
- });
630
- const result = outcome?.result;
631
- if (result) {
632
- console.log(JSON.stringify(result));
633
- } else {
634
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
635
- }
636
- }
637
-
638
- async function cmdPipelineList() {
639
- const outcome = await requestHub(HUB_OPERATIONS.pipelineList, {});
640
- const result = outcome?.result;
641
- if (result) {
642
- console.log(JSON.stringify(result));
643
- } else {
644
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
645
- }
646
- }
647
-
648
- async function cmdPing() {
649
- const outcome = await requestHub(HUB_OPERATIONS.hubStatus, { scope: 'hub' }, 2000);
650
-
651
- if (outcome?.transport === 'pipe' && outcome.result?.ok) {
652
- console.log(JSON.stringify({
653
- ok: true,
654
- hub: outcome.result.data?.hub?.state || 'healthy',
655
- pipe_path: getHubPipePath(),
656
- transport: 'pipe',
657
- }));
658
- return;
659
- }
660
-
661
- if (outcome?.transport === 'http' && outcome.result) {
662
- const data = outcome.result;
663
- console.log(JSON.stringify({
664
- ok: true,
665
- hub: data.hub?.state,
666
- sessions: data.sessions,
667
- pipe_path: data.pipe?.path || data.pipe_path || null,
668
- transport: 'http',
669
- }));
670
- return;
671
- }
672
-
673
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
674
- }
675
-
676
- export async function main(argv = process.argv.slice(2)) {
677
- const cmd = argv[0];
678
- const args = parseArgs(argv.slice(1));
679
-
680
- switch (cmd) {
681
- case 'register': await cmdRegister(args); break;
682
- case 'result': await cmdResult(args); break;
683
- case 'control': await cmdControl(args); break;
684
- case 'context': await cmdContext(args); break;
685
- case 'deregister': await cmdDeregister(args); break;
686
- case 'assign-async': await cmdAssignAsync(args); break;
687
- case 'assign-result': await cmdAssignResult(args); break;
688
- case 'assign-status': await cmdAssignStatus(args); break;
689
- case 'assign-retry': await cmdAssignRetry(args); break;
690
- case 'team-info': await cmdTeamInfo(args); break;
691
- case 'team-task-list': await cmdTeamTaskList(args); break;
692
- case 'team-task-update': await cmdTeamTaskUpdate(args); break;
693
- case 'team-send-message': await cmdTeamSendMessage(args); break;
694
- case 'pipeline-state': await cmdPipelineState(args); break;
695
- case 'pipeline-advance': await cmdPipelineAdvance(args); break;
696
- case 'pipeline-init': await cmdPipelineInit(args); break;
697
- case 'pipeline-list': await cmdPipelineList(args); break;
698
- case 'ping': await cmdPing(args); break;
699
- default:
700
- 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> [--옵션]');
701
- process.exit(1);
702
- }
703
- }
704
-
705
- const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/bridge.mjs');
706
- if (selfRun) {
707
- await main();
708
- }
1
+ #!/usr/bin/env node
2
+ // hub/bridge.mjs — tfx-route.sh ↔ tfx-hub 브릿지 CLI
3
+ //
4
+ // Named Pipe/Unix Socket 제어 채널을 우선 사용하고,
5
+ // 연결이 없을 때만 HTTP /bridge/* 엔드포인트로 내려간다.
6
+
7
+ import net from 'node:net';
8
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { parseArgs as nodeParseArgs } from 'node:util';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ import { getPipelineStateDbPath } from './pipeline/state.mjs';
16
+
17
+ const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
18
+ const HUB_TOKEN_FILE = join(homedir(), '.claude', '.tfx-hub-token');
19
+ const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url));
20
+
21
+ function normalizeToken(raw) {
22
+ if (raw == null) return null;
23
+ const token = String(raw).trim();
24
+ return token || null;
25
+ }
26
+
27
+ // Hub 인증 토큰 읽기 (파일 없으면 null → 하위 호환)
28
+ function readHubToken() {
29
+ const envToken = normalizeToken(process.env.TFX_HUB_TOKEN);
30
+ if (envToken) return envToken;
31
+ try {
32
+ return normalizeToken(readFileSync(HUB_TOKEN_FILE, 'utf8'));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export function getHubUrl() {
39
+ if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
40
+
41
+ if (existsSync(HUB_PID_FILE)) {
42
+ try {
43
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
44
+ return `http://${info.host || '127.0.0.1'}:${info.port || 27888}`;
45
+ } catch {
46
+ // 무시
47
+ }
48
+ }
49
+
50
+ const port = process.env.TFX_HUB_PORT || '27888';
51
+ return `http://127.0.0.1:${port}`;
52
+ }
53
+
54
+ export function getHubPipePath() {
55
+ if (process.env.TFX_HUB_PIPE) return process.env.TFX_HUB_PIPE;
56
+
57
+ if (!existsSync(HUB_PID_FILE)) return null;
58
+ try {
59
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
60
+ return info.pipe_path || info.pipePath || null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ const HUB_OPERATIONS = Object.freeze({
67
+ register: { transport: 'command', action: 'register', httpPath: '/bridge/register' },
68
+ result: { transport: 'command', action: 'result', httpPath: '/bridge/result' },
69
+ control: { transport: 'command', action: 'control', httpPath: '/bridge/control' },
70
+ context: { transport: 'query', action: 'drain', httpPath: '/bridge/context' },
71
+ deregister: { transport: 'command', action: 'deregister', httpPath: '/bridge/deregister' },
72
+ assignAsync: { transport: 'command', action: 'assign', httpPath: '/bridge/assign/async' },
73
+ assignResult: { transport: 'command', action: 'assign_result', httpPath: '/bridge/assign/result' },
74
+ assignStatus: { transport: 'query', action: 'assign_status', httpPath: '/bridge/assign/status' },
75
+ assignRetry: { transport: 'command', action: 'assign_retry', httpPath: '/bridge/assign/retry' },
76
+ teamInfo: { transport: 'query', action: 'team_info', httpPath: '/bridge/team/info' },
77
+ teamTaskList: { transport: 'query', action: 'team_task_list', httpPath: '/bridge/team/task-list' },
78
+ teamTaskUpdate: { transport: 'command', action: 'team_task_update', httpPath: '/bridge/team/task-update' },
79
+ teamSendMessage: { transport: 'command', action: 'team_send_message', httpPath: '/bridge/team/send-message' },
80
+ pipelineState: { transport: 'query', action: 'pipeline_state', httpPath: '/bridge/pipeline/state' },
81
+ pipelineAdvance: { transport: 'command', action: 'pipeline_advance', httpPath: '/bridge/pipeline/advance' },
82
+ pipelineInit: { transport: 'command', action: 'pipeline_init', httpPath: '/bridge/pipeline/init' },
83
+ pipelineList: { transport: 'query', action: 'pipeline_list', httpPath: '/bridge/pipeline/list' },
84
+ hubStatus: { transport: 'query', action: 'status', httpPath: '/status', httpMethod: 'GET' },
85
+ delegatorDelegate: { transport: 'command', action: 'delegator_delegate', httpPath: '/bridge/delegator/delegate' },
86
+ delegatorReply: { transport: 'command', action: 'delegator_reply', httpPath: '/bridge/delegator/reply' },
87
+ delegatorStatus: { transport: 'query', action: 'delegator_status', httpPath: '/bridge/delegator/status' },
88
+ });
89
+
90
+ export async function requestJson(path, { method = 'POST', body, timeoutMs = 5000 } = {}) {
91
+ const controller = new AbortController();
92
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
93
+
94
+ try {
95
+ const headers = {};
96
+ const token = readHubToken();
97
+ if (token) {
98
+ headers['Authorization'] = `Bearer ${token}`;
99
+ }
100
+ if (body !== undefined) {
101
+ headers['Content-Type'] = 'application/json';
102
+ }
103
+
104
+ const res = await fetch(`${getHubUrl()}${path}`, {
105
+ method,
106
+ headers,
107
+ body: body === undefined ? undefined : JSON.stringify(body),
108
+ signal: controller.signal,
109
+ });
110
+ clearTimeout(timer);
111
+ return await res.json();
112
+ } catch {
113
+ clearTimeout(timer);
114
+ return null;
115
+ }
116
+ }
117
+
118
+ export async function post(path, body, timeoutMs = 5000) {
119
+ return await requestJson(path, { method: 'POST', body, timeoutMs });
120
+ }
121
+
122
+ export async function connectPipe(timeoutMs = 1200) {
123
+ const pipePath = getHubPipePath();
124
+ if (!pipePath) return null;
125
+
126
+ return await new Promise((resolve) => {
127
+ const socket = net.createConnection(pipePath);
128
+ const timer = setTimeout(() => {
129
+ try { socket.destroy(); } catch {}
130
+ resolve(null);
131
+ }, timeoutMs);
132
+
133
+ socket.once('connect', () => {
134
+ clearTimeout(timer);
135
+ socket.setEncoding('utf8');
136
+ resolve(socket);
137
+ });
138
+
139
+ socket.once('error', () => {
140
+ clearTimeout(timer);
141
+ try { socket.destroy(); } catch {}
142
+ resolve(null);
143
+ });
144
+ });
145
+ }
146
+
147
+ async function pipeRequest(type, action, payload, timeoutMs = 3000) {
148
+ const socket = await connectPipe(Math.min(timeoutMs, 1500));
149
+ if (!socket) return null;
150
+
151
+ return await new Promise((resolve) => {
152
+ const requestId = randomUUID();
153
+ let buffer = '';
154
+ let settled = false;
155
+ const timer = setTimeout(() => {
156
+ finish(null);
157
+ }, timeoutMs);
158
+
159
+ const finish = (result) => {
160
+ if (settled) return;
161
+ settled = true;
162
+ clearTimeout(timer);
163
+ try { socket.end(); } catch {}
164
+ resolve(result);
165
+ };
166
+
167
+ socket.on('data', (chunk) => {
168
+ buffer += chunk;
169
+ let newlineIndex = buffer.indexOf('\n');
170
+ while (newlineIndex >= 0) {
171
+ const line = buffer.slice(0, newlineIndex).trim();
172
+ buffer = buffer.slice(newlineIndex + 1);
173
+ newlineIndex = buffer.indexOf('\n');
174
+ if (!line) continue;
175
+
176
+ let frame;
177
+ try {
178
+ frame = JSON.parse(line);
179
+ } catch {
180
+ continue;
181
+ }
182
+
183
+ if (frame?.type !== 'response' || frame.request_id !== requestId) continue;
184
+ finish({
185
+ ok: frame.ok,
186
+ error: frame.error,
187
+ data: frame.data,
188
+ });
189
+ return;
190
+ }
191
+ });
192
+
193
+ socket.on('error', () => finish(null));
194
+ socket.write(JSON.stringify({
195
+ type,
196
+ request_id: requestId,
197
+ payload: { action, ...payload },
198
+ }) + '\n');
199
+ });
200
+ }
201
+
202
+ async function pipeCommand(action, payload, timeoutMs = 3000) {
203
+ return await pipeRequest('command', action, payload, timeoutMs);
204
+ }
205
+
206
+ async function pipeQuery(action, payload, timeoutMs = 3000) {
207
+ return await pipeRequest('query', action, payload, timeoutMs);
208
+ }
209
+
210
+ export function parseArgs(argv) {
211
+ const { values } = nodeParseArgs({
212
+ args: argv,
213
+ options: {
214
+ agent: { type: 'string' },
215
+ cli: { type: 'string' },
216
+ timeout: { type: 'string' },
217
+ topics: { type: 'string' },
218
+ capabilities: { type: 'string' },
219
+ file: { type: 'string' },
220
+ payload: { type: 'string' },
221
+ topic: { type: 'string' },
222
+ trace: { type: 'string' },
223
+ correlation: { type: 'string' },
224
+ 'exit-code': { type: 'string' },
225
+ max: { type: 'string' },
226
+ out: { type: 'string' },
227
+ team: { type: 'string' },
228
+ 'task-id': { type: 'string' },
229
+ 'job-id': { type: 'string' },
230
+ owner: { type: 'string' },
231
+ status: { type: 'string' },
232
+ statuses: { type: 'string' },
233
+ claim: { type: 'boolean' },
234
+ actor: { type: 'string' },
235
+ command: { type: 'string' },
236
+ reason: { type: 'string' },
237
+ from: { type: 'string' },
238
+ to: { type: 'string' },
239
+ text: { type: 'string' },
240
+ task: { type: 'string' },
241
+ 'supervisor-agent': { type: 'string' },
242
+ 'worker-agent': { type: 'string' },
243
+ priority: { type: 'string' },
244
+ 'ttl-ms': { type: 'string' },
245
+ 'timeout-ms': { type: 'string' },
246
+ 'max-retries': { type: 'string' },
247
+ attempt: { type: 'string' },
248
+ result: { type: 'string' },
249
+ error: { type: 'string' },
250
+ metadata: { type: 'string' },
251
+ 'requested-by': { type: 'string' },
252
+ summary: { type: 'string' },
253
+ color: { type: 'string' },
254
+ limit: { type: 'string' },
255
+ 'include-internal': { type: 'boolean' },
256
+ subject: { type: 'string' },
257
+ description: { type: 'string' },
258
+ 'fix-max': { type: 'string' },
259
+ 'ralph-max': { type: 'string' },
260
+ 'active-form': { type: 'string' },
261
+ 'add-blocks': { type: 'string' },
262
+ 'add-blocked-by': { type: 'string' },
263
+ 'metadata-patch': { type: 'string' },
264
+ 'if-match-mtime-ms': { type: 'string' },
265
+ provider: { type: 'string' },
266
+ mode: { type: 'string' },
267
+ prompt: { type: 'string' },
268
+ reply: { type: 'string' },
269
+ done: { type: 'boolean' },
270
+ 'mcp-profile': { type: 'string' },
271
+ 'session-key': { type: 'string' },
272
+ },
273
+ strict: false,
274
+ });
275
+ return values;
276
+ }
277
+
278
+ export function parseJsonSafe(raw, fallback = null) {
279
+ if (!raw) return fallback;
280
+ try {
281
+ return JSON.parse(raw);
282
+ } catch {
283
+ return fallback;
284
+ }
285
+ }
286
+
287
+ async function requestHub(operation, body, timeoutMs = 3000, fallback = null) {
288
+ const viaPipe = operation.transport === 'command'
289
+ ? await pipeCommand(operation.action, body, timeoutMs)
290
+ : await pipeQuery(operation.action, body, timeoutMs);
291
+ if (viaPipe) {
292
+ return { transport: 'pipe', result: viaPipe };
293
+ }
294
+
295
+ const viaHttp = operation.httpPath
296
+ ? await requestJson(operation.httpPath, {
297
+ method: operation.httpMethod || 'POST',
298
+ body: operation.httpMethod === 'GET' ? undefined : body,
299
+ timeoutMs: Math.max(timeoutMs, 5000),
300
+ })
301
+ : null;
302
+ if (viaHttp) {
303
+ return { transport: 'http', result: viaHttp };
304
+ }
305
+
306
+ if (!fallback) return null;
307
+ const viaFallback = await fallback();
308
+ if (!viaFallback) return null;
309
+ return { transport: 'fallback', result: viaFallback };
310
+ }
311
+
312
+ function unavailableResult() {
313
+ return { ok: false, reason: 'hub_unavailable' };
314
+ }
315
+
316
+ function emitJson(payload) {
317
+ if (payload !== undefined) {
318
+ console.log(JSON.stringify(payload));
319
+ }
320
+ return payload?.ok !== false;
321
+ }
322
+
323
+ async function cmdRegister(args) {
324
+ const agentId = args.agent;
325
+ const timeoutSec = parseInt(args.timeout || '600', 10);
326
+ const outcome = await requestHub(HUB_OPERATIONS.register, {
327
+ agent_id: agentId,
328
+ cli: args.cli || 'other',
329
+ timeout_sec: timeoutSec,
330
+ heartbeat_ttl_ms: (timeoutSec + 120) * 1000,
331
+ topics: args.topics ? args.topics.split(',') : [],
332
+ capabilities: args.capabilities ? args.capabilities.split(',') : ['code'],
333
+ metadata: {
334
+ pid: process.ppid,
335
+ registered_at: Date.now(),
336
+ },
337
+ });
338
+ const result = outcome?.result;
339
+
340
+ if (result?.ok) {
341
+ return emitJson({
342
+ ok: true,
343
+ agent_id: agentId,
344
+ lease_expires_ms: result.data?.lease_expires_ms,
345
+ pipe_path: result.data?.pipe_path || getHubPipePath(),
346
+ });
347
+ }
348
+
349
+ return emitJson(result || unavailableResult());
350
+ }
351
+
352
+ async function cmdResult(args) {
353
+ let output = '';
354
+ if (args.file && existsSync(args.file)) {
355
+ output = readFileSync(args.file, 'utf8').slice(0, 49152);
356
+ }
357
+
358
+ const defaultPayload = {
359
+ agent_id: args.agent,
360
+ exit_code: parseInt(args['exit-code'] || '0', 10),
361
+ output_length: output.length,
362
+ output_preview: output.slice(0, 4096),
363
+ output_file: args.file || null,
364
+ completed_at: Date.now(),
365
+ };
366
+
367
+ const outcome = await requestHub(HUB_OPERATIONS.result, {
368
+ agent_id: args.agent,
369
+ topic: args.topic || 'task.result',
370
+ payload: args.payload ? parseJsonSafe(args.payload, defaultPayload) : defaultPayload,
371
+ trace_id: args.trace || undefined,
372
+ correlation_id: args.correlation || undefined,
373
+ });
374
+ const result = outcome?.result;
375
+
376
+ if (result?.ok) {
377
+ return emitJson({ ok: true, message_id: result.data?.message_id });
378
+ }
379
+
380
+ return emitJson(result || unavailableResult());
381
+ }
382
+
383
+ async function cmdControl(args) {
384
+ const outcome = await requestHub(HUB_OPERATIONS.control, {
385
+ from_agent: args.from || 'lead',
386
+ to_agent: args.to,
387
+ command: args.command,
388
+ reason: args.reason || '',
389
+ payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
390
+ trace_id: args.trace || undefined,
391
+ correlation_id: args.correlation || undefined,
392
+ ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
393
+ });
394
+ const result = outcome?.result;
395
+ return emitJson(result || unavailableResult());
396
+ }
397
+
398
+ async function cmdContext(args) {
399
+ const outcome = await requestHub(HUB_OPERATIONS.context, {
400
+ agent_id: args.agent,
401
+ topics: args.topics ? args.topics.split(',') : undefined,
402
+ max_messages: parseInt(args.max || '10', 10),
403
+ auto_ack: true,
404
+ });
405
+ const result = outcome?.result;
406
+
407
+ if (result?.ok && result.data?.messages?.length) {
408
+ const parts = result.data.messages.map((message, index) => {
409
+ const payload = typeof message.payload === 'string'
410
+ ? message.payload
411
+ : JSON.stringify(message.payload, null, 2);
412
+ return `=== Context ${index + 1}: ${message.from_agent || 'unknown'} (${message.topic || 'unknown'}) ===\n${payload}`;
413
+ });
414
+ const combined = parts.join('\n\n');
415
+
416
+ if (args.out) {
417
+ writeFileSync(args.out, combined, 'utf8');
418
+ return emitJson({ ok: true, count: result.data.messages.length, file: args.out });
419
+ } else {
420
+ console.log(combined);
421
+ return true;
422
+ }
423
+ }
424
+
425
+ if (result?.ok) {
426
+ if (args.out) {
427
+ return emitJson({ ok: true, count: 0 });
428
+ }
429
+ return true;
430
+ }
431
+
432
+ return emitJson(result || unavailableResult());
433
+ }
434
+
435
+ async function cmdDeregister(args) {
436
+ const outcome = await requestHub(HUB_OPERATIONS.deregister, {
437
+ agent_id: args.agent,
438
+ });
439
+ const result = outcome?.result;
440
+
441
+ if (result?.ok) {
442
+ return emitJson({ ok: true, agent_id: args.agent, status: 'offline' });
443
+ }
444
+
445
+ return emitJson(result || unavailableResult());
446
+ }
447
+
448
+ async function cmdAssignAsync(args) {
449
+ const outcome = await requestHub(HUB_OPERATIONS.assignAsync, {
450
+ supervisor_agent: args['supervisor-agent'],
451
+ worker_agent: args['worker-agent'],
452
+ task: args.task,
453
+ topic: args.topic || 'assign.job',
454
+ payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
455
+ priority: args.priority != null ? Number(args.priority) : undefined,
456
+ ttl_ms: args['ttl-ms'] != null ? Number(args['ttl-ms']) : undefined,
457
+ timeout_ms: args['timeout-ms'] != null ? Number(args['timeout-ms']) : undefined,
458
+ max_retries: args['max-retries'] != null ? Number(args['max-retries']) : undefined,
459
+ trace_id: args.trace || undefined,
460
+ correlation_id: args.correlation || undefined,
461
+ });
462
+ const result = outcome?.result;
463
+ return emitJson(result || unavailableResult());
464
+ }
465
+
466
+ async function cmdAssignResult(args) {
467
+ const outcome = await requestHub(HUB_OPERATIONS.assignResult, {
468
+ job_id: args['job-id'],
469
+ worker_agent: args['worker-agent'],
470
+ status: args.status,
471
+ attempt: args.attempt != null ? Number(args.attempt) : undefined,
472
+ result: args.result ? parseJsonSafe(args.result, null) : undefined,
473
+ error: args.error ? parseJsonSafe(args.error, null) : undefined,
474
+ payload: args.payload ? parseJsonSafe(args.payload, {}) : {},
475
+ metadata: args.metadata ? parseJsonSafe(args.metadata, {}) : {},
476
+ });
477
+ const result = outcome?.result;
478
+ return emitJson(result || unavailableResult());
479
+ }
480
+
481
+ async function cmdAssignStatus(args) {
482
+ const outcome = await requestHub(HUB_OPERATIONS.assignStatus, {
483
+ job_id: args['job-id'],
484
+ });
485
+ const result = outcome?.result;
486
+ return emitJson(result || unavailableResult());
487
+ }
488
+
489
+ async function cmdAssignRetry(args) {
490
+ const outcome = await requestHub(HUB_OPERATIONS.assignRetry, {
491
+ job_id: args['job-id'],
492
+ reason: args.reason,
493
+ requested_by: args['requested-by'],
494
+ });
495
+ const result = outcome?.result;
496
+ return emitJson(result || unavailableResult());
497
+ }
498
+
499
+ async function cmdTeamInfo(args) {
500
+ const body = {
501
+ team_name: args.team,
502
+ include_members: true,
503
+ include_paths: true,
504
+ };
505
+ const outcome = await requestHub(HUB_OPERATIONS.teamInfo, body, 3000, async () => {
506
+ const { teamInfo } = await import('./team/nativeProxy.mjs');
507
+ return await teamInfo(body);
508
+ });
509
+ const result = outcome?.result;
510
+ return emitJson(result || unavailableResult());
511
+ }
512
+
513
+ async function cmdTeamTaskList(args) {
514
+ const body = {
515
+ team_name: args.team,
516
+ owner: args.owner,
517
+ statuses: args.statuses ? args.statuses.split(',').map((status) => status.trim()).filter(Boolean) : [],
518
+ include_internal: !!args['include-internal'],
519
+ limit: parseInt(args.limit || '200', 10),
520
+ };
521
+ const outcome = await requestHub(HUB_OPERATIONS.teamTaskList, body, 3000, async () => {
522
+ const { teamTaskList } = await import('./team/nativeProxy.mjs');
523
+ return await teamTaskList(body);
524
+ });
525
+ const result = outcome?.result;
526
+ return emitJson(result || unavailableResult());
527
+ }
528
+
529
+ async function cmdTeamTaskUpdate(args) {
530
+ const body = {
531
+ team_name: args.team,
532
+ task_id: args['task-id'],
533
+ claim: !!args.claim,
534
+ owner: args.owner,
535
+ status: args.status,
536
+ subject: args.subject,
537
+ description: args.description,
538
+ activeForm: args['active-form'],
539
+ add_blocks: args['add-blocks'] ? args['add-blocks'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
540
+ add_blocked_by: args['add-blocked-by'] ? args['add-blocked-by'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
541
+ metadata_patch: args['metadata-patch'] ? parseJsonSafe(args['metadata-patch'], null) : undefined,
542
+ if_match_mtime_ms: args['if-match-mtime-ms'] != null ? Number(args['if-match-mtime-ms']) : undefined,
543
+ actor: args.actor,
544
+ };
545
+ const outcome = await requestHub(HUB_OPERATIONS.teamTaskUpdate, body, 3000, async () => {
546
+ const { teamTaskUpdate } = await import('./team/nativeProxy.mjs');
547
+ return await teamTaskUpdate(body);
548
+ });
549
+ const result = outcome?.result;
550
+ return emitJson(result || unavailableResult());
551
+ }
552
+
553
+ async function cmdTeamSendMessage(args) {
554
+ const body = {
555
+ team_name: args.team,
556
+ from: args.from,
557
+ to: args.to || 'team-lead',
558
+ text: args.text,
559
+ summary: args.summary,
560
+ color: args.color || 'blue',
561
+ };
562
+ const outcome = await requestHub(HUB_OPERATIONS.teamSendMessage, body, 3000, async () => {
563
+ const { teamSendMessage } = await import('./team/nativeProxy.mjs');
564
+ return await teamSendMessage(body);
565
+ });
566
+ const result = outcome?.result;
567
+ return emitJson(result || unavailableResult());
568
+ }
569
+
570
+ function getHubDbPath() {
571
+ return getPipelineStateDbPath(PROJECT_ROOT);
572
+ }
573
+
574
+ async function cmdPipelineState(args) {
575
+ const outcome = await requestHub(HUB_OPERATIONS.pipelineState, { team_name: args.team }, 3000, async () => {
576
+ try {
577
+ const { default: Database } = await import('better-sqlite3');
578
+ const { ensurePipelineTable, readPipelineState } = await import('./pipeline/state.mjs');
579
+ const dbPath = getHubDbPath();
580
+ if (!existsSync(dbPath)) {
581
+ return { ok: false, error: 'hub_db_not_found' };
582
+ }
583
+ const db = new Database(dbPath, { readonly: true });
584
+ ensurePipelineTable(db);
585
+ const state = readPipelineState(db, args.team);
586
+ db.close();
587
+ return state
588
+ ? { ok: true, data: state }
589
+ : { ok: false, error: 'pipeline_not_found' };
590
+ } catch (e) {
591
+ return { ok: false, error: e.message };
592
+ }
593
+ });
594
+ const result = outcome?.result;
595
+ return emitJson(result || unavailableResult());
596
+ }
597
+
598
+ async function cmdPipelineAdvance(args) {
599
+ const body = {
600
+ team_name: args.team,
601
+ phase: args.status, // --status를 phase로 재활용
602
+ };
603
+ const outcome = await requestHub(HUB_OPERATIONS.pipelineAdvance, body, 3000, async () => {
604
+ try {
605
+ const { default: Database } = await import('better-sqlite3');
606
+ const { createPipeline } = await import('./pipeline/index.mjs');
607
+ const dbPath = getHubDbPath();
608
+ if (!existsSync(dbPath)) {
609
+ return { ok: false, error: 'hub_db_not_found' };
610
+ }
611
+ const db = new Database(dbPath);
612
+ const pipeline = createPipeline(db, args.team);
613
+ const advanceResult = pipeline.advance(args.status);
614
+ db.close();
615
+ return advanceResult;
616
+ } catch (e) {
617
+ return { ok: false, error: e.message };
618
+ }
619
+ });
620
+ const result = outcome?.result;
621
+ return emitJson(result || unavailableResult());
622
+ }
623
+
624
+ async function cmdPipelineInit(args) {
625
+ const outcome = await requestHub(HUB_OPERATIONS.pipelineInit, {
626
+ team_name: args.team,
627
+ fix_max: args['fix-max'] != null ? Number(args['fix-max']) : undefined,
628
+ ralph_max: args['ralph-max'] != null ? Number(args['ralph-max']) : undefined,
629
+ });
630
+ const result = outcome?.result;
631
+ return emitJson(result || unavailableResult());
632
+ }
633
+
634
+ async function cmdPipelineList() {
635
+ const outcome = await requestHub(HUB_OPERATIONS.pipelineList, {});
636
+ const result = outcome?.result;
637
+ return emitJson(result || unavailableResult());
638
+ }
639
+
640
+ async function cmdPing() {
641
+ const outcome = await requestHub(HUB_OPERATIONS.hubStatus, { scope: 'hub' }, 2000);
642
+
643
+ if (outcome?.transport === 'pipe' && outcome.result?.ok) {
644
+ return emitJson({
645
+ ok: true,
646
+ hub: outcome.result.data?.hub?.state || 'healthy',
647
+ pipe_path: getHubPipePath(),
648
+ transport: 'pipe',
649
+ });
650
+ }
651
+
652
+ if (outcome?.transport === 'http' && outcome.result) {
653
+ const data = outcome.result;
654
+ return emitJson({
655
+ ok: true,
656
+ hub: data.hub?.state,
657
+ sessions: data.sessions,
658
+ pipe_path: data.pipe?.path || data.pipe_path || null,
659
+ transport: 'http',
660
+ });
661
+ }
662
+
663
+ return emitJson(unavailableResult());
664
+ }
665
+
666
+ async function cmdDelegatorDelegate(args) {
667
+ const body = {
668
+ prompt: args.text || args.prompt,
669
+ provider: args.provider || 'auto',
670
+ mode: args.mode || 'sync',
671
+ agent_type: args.agent || 'executor',
672
+ mcp_profile: args['mcp-profile'] || 'auto',
673
+ session_key: args['session-key'] || undefined,
674
+ timeout_ms: args['timeout-ms'] != null ? Number(args['timeout-ms']) : undefined,
675
+ };
676
+ const timeoutMs = body.mode === 'async' ? 10000 : 120000;
677
+ const outcome = await requestHub(HUB_OPERATIONS.delegatorDelegate, body, timeoutMs);
678
+ return emitJson(outcome?.result || unavailableResult());
679
+ }
680
+
681
+ async function cmdDelegatorReply(args) {
682
+ const body = {
683
+ job_id: args['job-id'],
684
+ reply: args.text || args.reply,
685
+ done: !!args.done,
686
+ };
687
+ const outcome = await requestHub(HUB_OPERATIONS.delegatorReply, body, 120000);
688
+ return emitJson(outcome?.result || unavailableResult());
689
+ }
690
+
691
+ async function cmdDelegatorStatus(args) {
692
+ const body = {
693
+ job_id: args['job-id'],
694
+ };
695
+ const outcome = await requestHub(HUB_OPERATIONS.delegatorStatus, body, 5000);
696
+ return emitJson(outcome?.result || unavailableResult());
697
+ }
698
+
699
+ export async function main(argv = process.argv.slice(2)) {
700
+ const cmd = argv[0];
701
+ const args = parseArgs(argv.slice(1));
702
+
703
+ switch (cmd) {
704
+ case 'register': return await cmdRegister(args);
705
+ case 'result': return await cmdResult(args);
706
+ case 'control': return await cmdControl(args);
707
+ case 'context': return await cmdContext(args);
708
+ case 'deregister': return await cmdDeregister(args);
709
+ case 'assign-async': return await cmdAssignAsync(args);
710
+ case 'assign-result': return await cmdAssignResult(args);
711
+ case 'assign-status': return await cmdAssignStatus(args);
712
+ case 'assign-retry': return await cmdAssignRetry(args);
713
+ case 'team-info': return await cmdTeamInfo(args);
714
+ case 'team-task-list': return await cmdTeamTaskList(args);
715
+ case 'team-task-update': return await cmdTeamTaskUpdate(args);
716
+ case 'team-send-message': return await cmdTeamSendMessage(args);
717
+ case 'pipeline-state': return await cmdPipelineState(args);
718
+ case 'pipeline-advance': return await cmdPipelineAdvance(args);
719
+ case 'pipeline-init': return await cmdPipelineInit(args);
720
+ case 'pipeline-list': return await cmdPipelineList(args);
721
+ case 'ping': return await cmdPing(args);
722
+ case 'delegator-delegate': return await cmdDelegatorDelegate(args);
723
+ case 'delegator-reply': return await cmdDelegatorReply(args);
724
+ case 'delegator-status': return await cmdDelegatorStatus(args);
725
+ default:
726
+ 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> [--옵션]');
727
+ process.exit(1);
728
+ }
729
+ }
730
+
731
+ const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/bridge.mjs');
732
+ if (selfRun) {
733
+ process.exitCode = await main() ? 0 : 1;
734
+ }