triflux 3.1.0-dev.3 → 3.1.0-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hub/bridge.mjs CHANGED
@@ -14,15 +14,16 @@
14
14
  // Hub 미실행 시 모든 커맨드는 조용히 실패 (exit 0).
15
15
  // tfx-route.sh 흐름을 절대 차단하지 않는다.
16
16
 
17
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
18
- import { join } from 'node:path';
19
- import { homedir } from 'node:os';
17
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+ import { parseArgs as nodeParseArgs } from 'node:util';
20
21
 
21
22
  const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
22
23
 
23
24
  // ── Hub URL 해석 ──
24
25
 
25
- function getHubUrl() {
26
+ function getHubUrl() {
26
27
  // 환경변수 우선
27
28
  if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
28
29
 
@@ -35,16 +36,18 @@ function getHubUrl() {
35
36
  }
36
37
 
37
38
  // 기본값
38
- const port = process.env.TFX_HUB_PORT || '27888';
39
- return `http://127.0.0.1:${port}`;
40
- }
41
-
42
- // ── HTTP 요청 ──
43
-
44
- async function post(path, body, timeoutMs = 5000) {
45
- const url = `${getHubUrl()}${path}`;
46
- const controller = new AbortController();
47
- const timer = setTimeout(() => controller.abort(), timeoutMs);
39
+ const port = process.env.TFX_HUB_PORT || '27888';
40
+ return `http://127.0.0.1:${port}`;
41
+ }
42
+
43
+ const _cachedHubUrl = getHubUrl();
44
+
45
+ // ── HTTP 요청 ──
46
+
47
+ async function post(path, body, timeoutMs = 5000) {
48
+ const url = `${_cachedHubUrl}${path}`;
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
48
51
 
49
52
  try {
50
53
  const res = await fetch(url, {
@@ -63,22 +66,27 @@ async function post(path, body, timeoutMs = 5000) {
63
66
 
64
67
  // ── 인자 파싱 ──
65
68
 
66
- function parseArgs(argv) {
67
- const args = {};
68
- for (let i = 0; i < argv.length; i++) {
69
- if (argv[i].startsWith('--')) {
70
- const key = argv[i].slice(2);
71
- const next = argv[i + 1];
72
- if (!next || next.startsWith('--')) {
73
- args[key] = true;
74
- } else {
75
- args[key] = next;
76
- i++;
77
- }
78
- }
79
- }
80
- return args;
81
- }
69
+ function parseArgs(argv) {
70
+ const { values } = nodeParseArgs({
71
+ args: argv,
72
+ options: {
73
+ agent: { type: 'string' },
74
+ cli: { type: 'string' },
75
+ timeout: { type: 'string' },
76
+ topics: { type: 'string' },
77
+ capabilities: { type: 'string' },
78
+ file: { type: 'string' },
79
+ topic: { type: 'string' },
80
+ trace: { type: 'string' },
81
+ correlation: { type: 'string' },
82
+ 'exit-code': { type: 'string' },
83
+ max: { type: 'string' },
84
+ out: { type: 'string' },
85
+ },
86
+ strict: false,
87
+ });
88
+ return values;
89
+ }
82
90
 
83
91
  // ── 커맨드 ──
84
92
 
@@ -127,14 +135,14 @@ async function cmdResult(args) {
127
135
  const result = await post('/bridge/result', {
128
136
  agent_id: agentId,
129
137
  topic,
130
- payload: {
131
- agent_id: agentId,
132
- exit_code: exitCode,
133
- output_length: output.length,
134
- output_preview: output.slice(0, 4096), // 미리보기 4KB
135
- output_full: output, // 전체 (최대 48KB)
136
- completed_at: Date.now(),
137
- },
138
+ payload: {
139
+ agent_id: agentId,
140
+ exit_code: exitCode,
141
+ output_length: output.length,
142
+ output_preview: output.slice(0, 4096), // 미리보기 4KB
143
+ output_file: filePath || null,
144
+ completed_at: Date.now(),
145
+ },
138
146
  trace_id: traceId,
139
147
  correlation_id: correlationId,
140
148
  });
@@ -193,10 +201,10 @@ async function cmdDeregister(args) {
193
201
  }
194
202
  }
195
203
 
196
- async function cmdPing() {
197
- try {
198
- const url = `${getHubUrl()}/status`;
199
- const controller = new AbortController();
204
+ async function cmdPing() {
205
+ try {
206
+ const url = `${_cachedHubUrl}/status`;
207
+ const controller = new AbortController();
200
208
  const timer = setTimeout(() => controller.abort(), 3000);
201
209
  const res = await fetch(url, { signal: controller.signal });
202
210
  clearTimeout(timer);
package/hub/hitl.mjs CHANGED
@@ -1,6 +1,5 @@
1
- // hub/hitl.mjs — Human-in-the-Loop 매니저
2
- // 사용자 입력 요청/응답, 타임아웃 자동 처리
3
- import { uuidv7 } from './store.mjs';
1
+ // hub/hitl.mjs — Human-in-the-Loop 매니저
2
+ // 사용자 입력 요청/응답, 타임아웃 자동 처리
4
3
 
5
4
  /**
6
5
  * HITL 매니저 생성
@@ -88,39 +87,36 @@ export function createHitlManager(store) {
88
87
  * 만료된 요청 자동 처리
89
88
  * deadline 초과 시 default_action 적용
90
89
  */
91
- checkTimeouts() {
92
- const pending = store.getPendingHumanRequests();
93
- const now = Date.now();
94
- let processed = 0;
95
-
96
- for (const hr of pending) {
97
- if (hr.deadline_ms > now) continue;
98
-
99
- // default_action 적용
100
- if (hr.default_action === 'timeout_continue') {
101
- store.updateHumanRequest(hr.request_id, 'timed_out', null);
102
- // 요청자에게 타임아웃 알림
103
- const msg = store.enqueueMessage({
104
- type: 'human_response',
105
- from: 'hub:hitl',
106
- to: hr.requester_agent,
107
- topic: 'human.response',
108
- priority: 5,
109
- ttl_ms: 300000,
110
- payload: { request_id: hr.request_id, action: 'timeout_continue', content: null },
111
- correlation_id: hr.correlation_id,
112
- trace_id: hr.trace_id,
113
- });
114
- store.deliverToAgent(msg.id, hr.requester_agent);
115
- } else {
116
- // decline 또는 cancel
117
- store.updateHumanRequest(hr.request_id, 'timed_out', null);
118
- }
119
- processed++;
120
- }
121
-
122
- return processed;
123
- },
90
+ checkTimeouts() {
91
+ const pending = store.getPendingHumanRequests();
92
+ const now = Date.now();
93
+ const expired = pending.filter(hr => hr.deadline_ms <= now);
94
+ if (!expired.length) return 0;
95
+
96
+ // 트랜잭션으로 만료 요청을 일괄 처리해 DB 왕복을 줄인다.
97
+ const processExpired = store.db.transaction(() => {
98
+ for (const hr of expired) {
99
+ store.updateHumanRequest(hr.request_id, 'timed_out', null);
100
+ if (hr.default_action === 'timeout_continue') {
101
+ const msg = store.enqueueMessage({
102
+ type: 'human_response',
103
+ from: 'hub:hitl',
104
+ to: hr.requester_agent,
105
+ topic: 'human.response',
106
+ priority: 5,
107
+ ttl_ms: 300000,
108
+ payload: { request_id: hr.request_id, action: 'timeout_continue', content: null },
109
+ correlation_id: hr.correlation_id,
110
+ trace_id: hr.trace_id,
111
+ });
112
+ store.deliverToAgent(msg.id, hr.requester_agent);
113
+ }
114
+ }
115
+ return expired.length;
116
+ });
117
+
118
+ return processExpired();
119
+ },
124
120
 
125
121
  /** 대기 중인 요청 목록 */
126
122
  getPendingRequests() {
package/hub/router.mjs CHANGED
@@ -1,14 +1,17 @@
1
- // hub/router.mjs — Actor mailbox 라우터 + QoS 스케줄러
2
- // 메시지 라우팅, ask/publish/handoff 처리, TTL 정리
3
- import { uuidv7 } from './store.mjs';
1
+ // hub/router.mjs — Actor mailbox 라우터 + QoS 스케줄러
2
+ // 메시지 라우팅, ask/publish/handoff 처리, TTL 정리
3
+ import { EventEmitter, once } from 'node:events';
4
+ import { uuidv7 } from './store.mjs';
4
5
 
5
6
  /**
6
7
  * 라우터 생성
7
8
  * @param {object} store — createStore() 반환 객체
8
9
  */
9
- export function createRouter(store) {
10
- let sweepTimer = null;
11
- let staleTimer = null;
10
+ export function createRouter(store) {
11
+ let sweepTimer = null;
12
+ let staleTimer = null;
13
+ const responseEmitter = new EventEmitter();
14
+ responseEmitter.setMaxListeners(200);
12
15
 
13
16
  const router = {
14
17
  /**
@@ -54,24 +57,29 @@ export function createRouter(store) {
54
57
  };
55
58
  }
56
59
 
57
- // 짧은 폴링 대기 (최대 30초 제한)
58
- const deadline = Date.now() + Math.min(await_response_ms, 30000);
59
- while (Date.now() < deadline) {
60
- const resp = store.getResponseByCorrelation(cid);
61
- if (resp) {
62
- return {
63
- ok: true,
64
- data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response: resp.payload },
65
- };
66
- }
67
- await new Promise(r => setTimeout(r, 100));
68
- }
69
-
70
- // 타임아웃 — 티켓 반환
71
- return {
72
- ok: true,
73
- data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'delivered' },
74
- };
60
+ // 이벤트 기반 대기 (최대 30초 제한)
61
+ try {
62
+ const [payload] = await once(responseEmitter, cid, {
63
+ signal: AbortSignal.timeout(Math.min(await_response_ms, 30000)),
64
+ });
65
+ return {
66
+ ok: true,
67
+ data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response: payload },
68
+ };
69
+ } catch {
70
+ // 타임아웃 DB에서 최종 확인
71
+ const resp = store.getResponseByCorrelation(cid);
72
+ if (resp) {
73
+ return {
74
+ ok: true,
75
+ data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'answered', response: resp.payload },
76
+ };
77
+ }
78
+ return {
79
+ ok: true,
80
+ data: { request_message_id: msg.id, correlation_id: cid, trace_id: tid, state: 'delivered' },
81
+ };
82
+ }
75
83
  },
76
84
 
77
85
  /**
@@ -83,16 +91,19 @@ export function createRouter(store) {
83
91
  payload = {}, trace_id, correlation_id,
84
92
  }) {
85
93
  const type = correlation_id ? 'response' : 'event';
86
- const msg = store.enqueueMessage({
87
- type, from, to, topic, priority, ttl_ms, payload,
88
- correlation_id: correlation_id || uuidv7(),
89
- trace_id: trace_id || uuidv7(),
90
- });
91
- const fanout = router.route(msg);
92
- return {
93
- ok: true,
94
- data: { message_id: msg.id, fanout_count: fanout, expires_at_ms: msg.expires_at_ms },
95
- };
94
+ const msg = store.enqueueMessage({
95
+ type, from, to, topic, priority, ttl_ms, payload,
96
+ correlation_id: correlation_id || uuidv7(),
97
+ trace_id: trace_id || uuidv7(),
98
+ });
99
+ const fanout = router.route(msg);
100
+ if (correlation_id) {
101
+ responseEmitter.emit(correlation_id, msg.payload);
102
+ }
103
+ return {
104
+ ok: true,
105
+ data: { message_id: msg.id, fanout_count: fanout, expires_at_ms: msg.expires_at_ms },
106
+ };
96
107
  },
97
108
 
98
109
  /**
@@ -118,15 +129,15 @@ export function createRouter(store) {
118
129
 
119
130
  // ── 스위퍼 ──
120
131
 
121
- /** 주기적 만료 정리 시작 (1초: 메시지, 60초: 비활성 에이전트) */
122
- startSweeper() {
123
- if (sweepTimer) return;
124
- sweepTimer = setInterval(() => {
125
- try { store.sweepExpired(); } catch { /* 무시 */ }
126
- }, 1000);
127
- staleTimer = setInterval(() => {
128
- try { store.sweepStaleAgents(); } catch { /* 무시 */ }
129
- }, 120000);
132
+ /** 주기적 만료 정리 시작 (10초: 메시지, 60초: 비활성 에이전트) */
133
+ startSweeper() {
134
+ if (sweepTimer) return;
135
+ sweepTimer = setInterval(() => {
136
+ try { store.sweepExpired(); } catch { /* 무시 */ }
137
+ }, 10000);
138
+ staleTimer = setInterval(() => {
139
+ try { store.sweepStaleAgents(); } catch { /* 무시 */ }
140
+ }, 120000);
130
141
  sweepTimer.unref();
131
142
  staleTimer.unref();
132
143
  },
@@ -185,5 +196,5 @@ export function createRouter(store) {
185
196
  },
186
197
  };
187
198
 
188
- return router;
189
- }
199
+ return { ...router, responseEmitter };
200
+ }
package/hub/server.mjs CHANGED
@@ -214,18 +214,23 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1' } = {}
214
214
  if (req.method === 'POST') {
215
215
  const body = await parseBody(req);
216
216
 
217
- if (sessionId && transports.has(sessionId)) {
218
- // 기존 세션
219
- await transports.get(sessionId).handleRequest(req, res, body);
220
- } else if (!sessionId && isInitializeRequest(body)) {
221
- // 세션 초기화
222
- const transport = new StreamableHTTPServerTransport({
223
- sessionIdGenerator: () => randomUUID(),
224
- onsessioninitialized: (sid) => { transports.set(sid, transport); },
225
- });
226
- transport.onclose = () => {
227
- if (transport.sessionId) transports.delete(transport.sessionId);
228
- };
217
+ if (sessionId && transports.has(sessionId)) {
218
+ // 기존 세션
219
+ const t = transports.get(sessionId);
220
+ t._lastActivity = Date.now();
221
+ await t.handleRequest(req, res, body);
222
+ } else if (!sessionId && isInitializeRequest(body)) {
223
+ // 세션 초기화
224
+ const transport = new StreamableHTTPServerTransport({
225
+ sessionIdGenerator: () => randomUUID(),
226
+ onsessioninitialized: (sid) => {
227
+ transport._lastActivity = Date.now();
228
+ transports.set(sid, transport);
229
+ },
230
+ });
231
+ transport.onclose = () => {
232
+ if (transport.sessionId) transports.delete(transport.sessionId);
233
+ };
229
234
  const mcp = createMcpForSession();
230
235
  await mcp.connect(transport);
231
236
  await transport.handleRequest(req, res, body);
@@ -283,17 +288,18 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1' } = {}
283
288
  }, 10000);
284
289
  hitlTimer.unref();
285
290
 
286
- // stale 세션 정리 (60초 주기 transport.onclose 미호출 대비)
287
- const sessionTimer = setInterval(() => {
288
- for (const [sid, transport] of transports) {
289
- try {
290
- if (transport._writableState?.destroyed || transport._readableState?.destroyed) {
291
- transports.delete(sid);
292
- }
293
- } catch { transports.delete(sid); }
294
- }
295
- }, 60000);
296
- sessionTimer.unref();
291
+ // 비활성 세션 정리 (60초 주기, 30분 TTL)
292
+ const SESSION_TTL_MS = 30 * 60 * 1000;
293
+ const sessionTimer = setInterval(() => {
294
+ const now = Date.now();
295
+ for (const [sid, transport] of transports) {
296
+ if (now - (transport._lastActivity || 0) > SESSION_TTL_MS) {
297
+ try { transport.close(); } catch {}
298
+ transports.delete(sid);
299
+ }
300
+ }
301
+ }, 60000);
302
+ sessionTimer.unref();
297
303
 
298
304
  // PID 파일 기록
299
305
  mkdirSync(PID_DIR, { recursive: true });
package/hub/store.mjs CHANGED
@@ -6,12 +6,23 @@ import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { randomBytes } from 'node:crypto';
8
8
 
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
-
11
- /** UUIDv7 생성 (RFC 9562) */
12
- export function uuidv7() {
13
- const now = BigInt(Date.now());
14
- const buf = randomBytes(16);
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ let _rndPool = Buffer.alloc(0), _rndOff = 0;
11
+
12
+ function pooledRandom(n) {
13
+ if (_rndOff + n > _rndPool.length) {
14
+ _rndPool = randomBytes(256);
15
+ _rndOff = 0;
16
+ }
17
+ const out = Buffer.from(_rndPool.subarray(_rndOff, _rndOff + n));
18
+ _rndOff += n;
19
+ return out;
20
+ }
21
+
22
+ /** UUIDv7 생성 (RFC 9562) */
23
+ export function uuidv7() {
24
+ const now = BigInt(Date.now());
25
+ const buf = pooledRandom(16);
15
26
  buf[0] = Number((now >> 40n) & 0xffn);
16
27
  buf[1] = Number((now >> 32n) & 0xffn);
17
28
  buf[2] = Number((now >> 24n) & 0xffn);
@@ -62,9 +73,20 @@ export function createStore(dbPath) {
62
73
  db.pragma('busy_timeout = 5000');
63
74
  db.pragma('wal_autocheckpoint = 1000');
64
75
 
65
- // 스키마 초기화 (schema.sql 전체 실행 — 주석 포함 안전 처리)
66
- const schemaSQL = readFileSync(join(__dirname, 'schema.sql'), 'utf8');
67
- db.exec(schemaSQL);
76
+ // 스키마 초기화 (schema.sql 전체 실행 — 주석 포함 안전 처리)
77
+ const schemaSQL = readFileSync(join(__dirname, 'schema.sql'), 'utf8');
78
+
79
+ // 스키마 버전 체크 — 불필요한 재실행 방지
80
+ db.exec("CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)");
81
+ const SCHEMA_VERSION = '1';
82
+ const curVer = (() => {
83
+ try { return db.prepare("SELECT value FROM _meta WHERE key='schema_version'").pluck().get(); }
84
+ catch { return null; }
85
+ })();
86
+ if (curVer !== SCHEMA_VERSION) {
87
+ db.exec(schemaSQL);
88
+ db.prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)").run(SCHEMA_VERSION);
89
+ }
68
90
 
69
91
  // ── 준비된 구문 ──
70
92
 
package/hub/tools.mjs CHANGED
@@ -145,15 +145,19 @@ export function createTools(store, router, hitl) {
145
145
  auto_ack: args.auto_ack,
146
146
  });
147
147
 
148
- // wait_ms > 0 이고 메시지 없으면 대기 재시도
149
- if (!messages.length && args.wait_ms > 0) {
150
- await new Promise(r => setTimeout(r, Math.min(args.wait_ms, 30000)));
151
- messages = store.pollForAgent(args.agent_id, {
152
- max_messages: args.max_messages,
153
- include_topics: args.include_topics,
154
- auto_ack: args.auto_ack,
155
- });
156
- }
148
+ // wait_ms > 0 이고 메시지 없으면 짧은 간격으로 반복 재시도
149
+ if (!messages.length && args.wait_ms > 0) {
150
+ const interval = Math.min(args.wait_ms, 500);
151
+ const deadline = Date.now() + Math.min(args.wait_ms, 30000);
152
+ while (!messages.length && Date.now() < deadline) {
153
+ await new Promise(r => setTimeout(r, interval));
154
+ messages = store.pollForAgent(args.agent_id, {
155
+ max_messages: args.max_messages,
156
+ include_topics: args.include_topics,
157
+ auto_ack: args.auto_ack,
158
+ });
159
+ }
160
+ }
157
161
 
158
162
  return {
159
163
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "3.1.0-dev.3",
3
+ "version": "3.1.0-dev.4",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/setup.mjs CHANGED
@@ -130,6 +130,19 @@ if (existsSync(hudPath)) {
130
130
  }
131
131
  }
132
132
 
133
+ // ── Stale PID 파일 정리 (hub 좀비 방지) ──
134
+
135
+ const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
136
+ if (existsSync(HUB_PID_FILE)) {
137
+ try {
138
+ const pidInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
139
+ process.kill(pidInfo.pid, 0); // 프로세스 존재 확인 (신호 미전송)
140
+ } catch {
141
+ try { unlinkSync(HUB_PID_FILE); } catch {} // 죽은 프로세스면 PID 파일 삭제
142
+ synced++;
143
+ }
144
+ }
145
+
133
146
  // ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 시) ──
134
147
 
135
148
  const cacheDir = join(CLAUDE_DIR, "cache");
@@ -1,15 +1,43 @@
1
- # tfx-hub — MCP 메시지 버스 관리
1
+ ---
2
+ name: tfx-hub
3
+ description: tfx-hub 개방형 스킬 — 커맨드(start/stop/status) + 자유형 작업 모두 처리
4
+ triggers:
5
+ - tfx-hub
6
+ argument-hint: "<start|stop|status|자유형 작업 설명>"
7
+ ---
8
+
9
+ # tfx-hub — MCP 메시지 버스 관리 + 개방형 작업
2
10
 
3
11
  > CLI 에이전트(Codex/Gemini/Claude) 간 실시간 메시지 허브를 관리합니다.
4
- > **tfx-auto와 완전 독립** 별도 스킬로 운영됩니다.
12
+ > **커맨드 매칭 + fallthrough**: start/stop/status에 매칭되면 즉시 실행,
13
+ > 매칭 안 되면 **hub 도메인 컨텍스트를 활용한 범용 작업**으로 처리합니다.
5
14
 
6
- ## 사용법
15
+ ## 입력 해석 규칙
7
16
 
8
17
  ```
9
- /tfx-hub start 허브 데몬 시작 (기본 포트 27888)
10
- /tfx-hub start --port 28000 ← 커스텀 포트
11
- /tfx-hub stop ← 허브 중지
12
- /tfx-hub status ← 상태/메트릭 확인
18
+ /tfx-hub start 커맨드 매칭 허브 시작
19
+ /tfx-hub stop → 커맨드 매칭 허브 중지
20
+ /tfx-hub status → 커맨드 매칭 → 상태 확인
21
+ /tfx-hub 테스트해줘 → fallthrough → hub 관련 범용 작업으로 처리
22
+ /tfx-hub 문서 저장해 → fallthrough → hub 관련 범용 작업으로 처리
23
+ /tfx-hub 브릿지 분석해 → fallthrough → hub 관련 범용 작업으로 처리
24
+ ```
25
+
26
+ **fallthrough 규칙**: 인자가 start/stop/status/--port 등 커맨드 키워드에 매칭되지 않으면,
27
+ 사용자의 입력을 **hub/브릿지/메시지버스 도메인의 자유형 작업**으로 해석한다.
28
+
29
+ fallthrough 라우팅:
30
+ ```bash
31
+ # tfx-route.sh 경유 (권장)
32
+ Bash("bash ~/.claude/scripts/tfx-route.sh {에이전트} '{hub 컨텍스트 + 작업}' {mcp_profile}")
33
+
34
+ # codex 직접 호출 시 — 반드시 exec 서브커맨드 포함
35
+ Bash("codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check '{작업}'")
36
+ Bash("codex --profile xhigh exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check '{작업}'")
37
+ # ↑ --profile은 exec 앞에, --skip-git-repo-check은 exec 뒤에
38
+
39
+ # Claude 네이티브 (탐색/검증)
40
+ Agent(subagent_type="oh-my-claudecode:explore", prompt="{작업}")
13
41
  ```
14
42
 
15
43
  ## 커맨드
@@ -78,6 +106,28 @@ claude mcp add --transport http tfx-hub http://127.0.0.1:27888/mcp
78
106
  | `request_human_input` | 사용자 입력 요청 (CAPTCHA/승인) |
79
107
  | `submit_human_input` | 사용자 입력 응답 |
80
108
 
109
+ ## 브릿지 REST 엔드포인트 (4개)
110
+
111
+ | 엔드포인트 | 설명 |
112
+ |-----------|------|
113
+ | `POST /bridge/register` | 에이전트 등록 (프로세스 수명 기반 lease) |
114
+ | `POST /bridge/result` | 결과 발행 (topic fanout) |
115
+ | `POST /bridge/context` | 선행 컨텍스트 폴링 (auto_ack) |
116
+ | `POST /bridge/deregister` | 에이전트 offline 마킹 |
117
+
118
+ ## 프로젝트 구조
119
+
120
+ ```
121
+ hub/
122
+ ├── server.mjs # MCP 서버 + REST 브릿지 엔드포인트
123
+ ├── store.mjs # SQLite WAL 상태 저장소
124
+ ├── router.mjs # Actor mailbox 라우터 + QoS
125
+ ├── tools.mjs # MCP 도구 8개 정의
126
+ ├── hitl.mjs # Human-in-the-Loop 매니저
127
+ ├── bridge.mjs # tfx-route.sh ↔ hub 브릿지 CLI
128
+ └── schema.sql # DB 스키마
129
+ ```
130
+
81
131
  ## 상태
82
132
 
83
133
  **dev 전용** — 로컬 테스트 목적. 프로덕션 배포 전 안정화 필요.