triflux 10.9.0 → 10.9.2

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/bin/triflux.mjs CHANGED
@@ -1047,14 +1047,13 @@ function cmdSetup(options = {}) {
1047
1047
  }
1048
1048
  {
1049
1049
  const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
1050
- if (claudeGuide.skipped)
1050
+ if (claudeGuide.skipped && claudeGuide.reason !== "global_sync_disabled")
1051
1051
  warn(`CLAUDE.md 라우팅 섹션 확인 실패: ${claudeGuide.reason}`);
1052
1052
  else if (
1053
1053
  claudeGuide.action === "created" ||
1054
1054
  claudeGuide.action === "updated"
1055
1055
  )
1056
1056
  ok("CLAUDE.md: 전역 triflux 라우팅 요약 갱신");
1057
- else ok("CLAUDE.md: 전역 triflux 라우팅 요약 유지");
1058
1057
  }
1059
1058
 
1060
1059
  // 스킬 동기화 (~/.claude/skills/{name}/SKILL.md)
@@ -1847,7 +1846,7 @@ async function cmdDoctor(options = {}) {
1847
1846
  }
1848
1847
  {
1849
1848
  const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
1850
- if (claudeGuide.skipped)
1849
+ if (claudeGuide.skipped && claudeGuide.reason !== "global_sync_disabled")
1851
1850
  warn(`CLAUDE.md 라우팅 섹션 확인 실패: ${claudeGuide.reason}`);
1852
1851
  else if (
1853
1852
  claudeGuide.action === "created" ||
@@ -167,8 +167,8 @@
167
167
  "priority": 2,
168
168
  "enabled": true,
169
169
  "timeout": 8,
170
- "blocking": false,
171
- "description": "tfx-hub 서비스 헬스체크 및 시작"
170
+ "blocking": true,
171
+ "description": "tfx-hub 서비스 헬스체크 및 시작 (BLOCKING — 세션 시작 전 Hub 준비 보장)"
172
172
  },
173
173
  {
174
174
  "id": "tfx-preflight-cache",
@@ -4,8 +4,8 @@
4
4
  // 6개 훅을 1개 node 프로세스 안에서 실행하여 콜드스타트 7회 → 1회로 줄인다.
5
5
  //
6
6
  // 분류:
7
- // BLOCKING (직렬, stdout 반환 전 완료): setup.runCritical, mcp-safety-guard.run
8
- // DEFERRED (병렬, 실패해도 안 죽음): hub-ensure.run, mcp-gateway-ensure.run, setup.runDeferred
7
+ // BLOCKING (직렬, stdout 반환 전 완료): setup.runCritical, mcp-safety-guard.run, hub-ensure.run
8
+ // DEFERRED (병렬, 실패해도 안 죽음): mcp-gateway-ensure.run, setup.runDeferred
9
9
  // BACKGROUND (fire-and-forget): preflight-cache.run
10
10
  //
11
11
  // external source 훅 (session-vault 등)은 여전히 execFile로 실행된다.
@@ -55,6 +55,24 @@ async function runBlocking(stdinData) {
55
55
  log.error({ hook: "mcp-safety-guard", err: String(err.message || err) }, "hook.failed");
56
56
  }
57
57
 
58
+ // 3. hub-ensure — Hub 필수 인프라, BLOCKING으로 실행
59
+ try {
60
+ const t0 = performance.now();
61
+ const hubMod = await importMod(join(SCRIPTS, "hub-ensure.mjs"));
62
+ const result = await hubMod.run(stdinData);
63
+ const dur = performance.now() - t0;
64
+ timings.push({ hook: "hub-ensure", dur_ms: Math.round(dur) });
65
+ if (result?.stdout) output.stdout += result.stdout + "\n";
66
+ if (result?.stderr) output.stderr += result.stderr + "\n";
67
+ if (result?.code !== 0) {
68
+ log.warn({ hook: "hub-ensure", dur_ms: Math.round(dur), code: result?.code }, "hook.warn");
69
+ } else {
70
+ log.info({ hook: "hub-ensure", dur_ms: Math.round(dur) }, "hook.completed");
71
+ }
72
+ } catch (err) {
73
+ log.error({ hook: "hub-ensure", err: String(err.message || err) }, "hook.failed");
74
+ }
75
+
58
76
  return { ...output, timings };
59
77
  }
60
78
 
@@ -65,13 +83,6 @@ async function runBlocking(stdinData) {
65
83
  */
66
84
  function runDeferred(stdinData) {
67
85
  const tasks = [
68
- {
69
- name: "hub-ensure",
70
- fn: async () => {
71
- const mod = await importMod(join(SCRIPTS, "hub-ensure.mjs"));
72
- return mod.run(stdinData);
73
- },
74
- },
75
86
  {
76
87
  name: "mcp-gateway-ensure",
77
88
  fn: async () => {
@@ -134,7 +145,7 @@ export async function execute(stdinData, externalHooks = []) {
134
145
  runBackground(stdinData);
135
146
 
136
147
  const totalDur = performance.now() - totalStart;
137
- log.info({ total_ms: Math.round(totalDur), blocking_count: 2, deferred_count: 3, bg_count: 1 }, "session-start.done");
148
+ log.info({ total_ms: Math.round(totalDur), blocking_count: 3, deferred_count: 2, bg_count: 1 }, "session-start.done");
138
149
 
139
150
  return {
140
151
  stdout: blocking.stdout,
package/hub/pipe.mjs CHANGED
@@ -125,6 +125,7 @@ export function createPipeServer({
125
125
  heartbeatTtlMs = DEFAULT_HEARTBEAT_TTL_MS,
126
126
  delegatorService = null,
127
127
  hitlManager = null,
128
+ onActivity = null,
128
129
  } = {}) {
129
130
  if (!router) {
130
131
  throw new Error("router is required");
@@ -164,6 +165,7 @@ export function createPipeServer({
164
165
 
165
166
  function touchClient(client) {
166
167
  client.lastHeartbeatMs = Date.now();
168
+ if (onActivity) onActivity();
167
169
  }
168
170
 
169
171
  function resolveAgentId(client, payload) {
package/hub/server.mjs CHANGED
@@ -580,6 +580,7 @@ export async function startHub({
580
580
  sessionId,
581
581
  delegatorService,
582
582
  hitlManager: hitl,
583
+ onActivity: markRequestActivity,
583
584
  });
584
585
  const assignCallbacks = createAssignCallbackServer({ store, sessionId });
585
586
  const tools = createTools(store, router, hitl, pipe);
@@ -1660,6 +1661,23 @@ if (selfRun) {
1660
1661
  const port = parseInt(process.env.TFX_HUB_PORT || "27888", 10);
1661
1662
  const dbPath = process.env.TFX_HUB_DB || undefined;
1662
1663
 
1664
+ const cleanupPidFile = () => {
1665
+ try {
1666
+ unlinkSync(PID_FILE);
1667
+ } catch {}
1668
+ };
1669
+
1670
+ process.on("unhandledRejection", (err) => {
1671
+ hubLog.fatal({ err }, "hub.unhandledRejection");
1672
+ cleanupPidFile();
1673
+ process.exit(1);
1674
+ });
1675
+ process.on("uncaughtException", (err) => {
1676
+ hubLog.fatal({ err }, "hub.uncaughtException");
1677
+ cleanupPidFile();
1678
+ process.exit(1);
1679
+ });
1680
+
1663
1681
  startHub({ port, dbPath })
1664
1682
  .then((info) => {
1665
1683
  const shutdown = async (signal) => {
@@ -1678,6 +1696,7 @@ if (selfRun) {
1678
1696
  })
1679
1697
  .catch((error) => {
1680
1698
  hubLog.fatal({ err: error }, "hub.start_failed");
1699
+ cleanupPidFile();
1681
1700
  process.exit(1);
1682
1701
  });
1683
1702
  }
package/hub/state.mjs CHANGED
@@ -256,8 +256,9 @@ export async function acquireLock(options = {}) {
256
256
  const raw = readFileSync(lockPath, "utf8");
257
257
  const data = parseJson(raw, {});
258
258
  const stats = statSync(lockPath);
259
+ const STALE_LOCK_AGE_MS = 60_000;
259
260
  const staleByPid = !isPidAlive(data?.pid);
260
- const staleByAge = Date.now() - stats.mtimeMs > timeoutMs;
261
+ const staleByAge = Date.now() - stats.mtimeMs > STALE_LOCK_AGE_MS;
261
262
  if (staleByPid || staleByAge) {
262
263
  try {
263
264
  unlinkSync(lockPath);
@@ -138,9 +138,7 @@ async function main() {
138
138
  // 실측 데이터 추출
139
139
  const stdin = await stdinPromise;
140
140
  const contextView = buildContextUsageView(stdin, contextSnapshot);
141
- const claudeUsage = claudeUsageSnapshot.isStale
142
- ? { ...(claudeUsageSnapshot.data || {}), stale: true }
143
- : claudeUsageSnapshot.data;
141
+ const claudeUsage = claudeUsageSnapshot.data;
144
142
  const codexEmail = getCodexEmail();
145
143
  const geminiEmail = getGeminiEmail();
146
144
  const codexBuckets = codexSnapshot.buckets;
package/hud/renderers.mjs CHANGED
@@ -263,8 +263,6 @@ export function getMicroLine(
263
263
  combinedSvPct,
264
264
  ) {
265
265
  const ctxView = contextView || buildContextUsageView({}, null);
266
- const staleMarker = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
267
-
268
266
  // Claude 5h/1w
269
267
  const cF =
270
268
  claudeUsage?.fiveHourPercent != null
@@ -319,7 +317,7 @@ export function getMicroLine(
319
317
  `${bold(codexWhite("x"))}${dim(":")}${xVal} ` +
320
318
  `${bold(geminiBlue("g"))}${dim(":")}${gVal} ` +
321
319
  `${dim("sv:")}${sv} ` +
322
- `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}${staleMarker}`;
320
+ `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
323
321
  return truncateAnsi(line, cols);
324
322
  }
325
323
 
@@ -334,8 +332,6 @@ export function getClaudeRows(
334
332
  ) {
335
333
  const ctxView = contextView || buildContextUsageView({}, null);
336
334
  const prefix = `${bold(claudeOrange("c"))}:`;
337
- const staleMarker = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
338
-
339
335
  // 절약 퍼센트
340
336
  const svStr = formatSvPct(combinedSvPct || 0);
341
337
  const svSuffix = `${dim("sv:")}${svStr}`;
@@ -388,18 +384,18 @@ export function getClaudeRows(
388
384
  hasData && weeklyPercent != null
389
385
  ? colorByProvider(weeklyPercent, `${weeklyPercent}%`, claudeOrange)
390
386
  : dim("--");
391
- const quotaSection = `${fShort}${dim("/")}${wShort}${staleMarker}`;
387
+ const quotaSection = `${fShort}${dim("/")}${wShort}`;
392
388
  return [{ prefix, left: quotaSection, right: "" }];
393
389
  }
394
390
 
395
391
  if (currentTier === "minimal") {
396
- const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleMarker}`;
392
+ const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}`;
397
393
  const right = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
398
394
  return [{ prefix, left: quotaSection, right }];
399
395
  }
400
396
 
401
397
  if (currentTier === "compact") {
402
- const quotaSection = `${dim("5h:")}${fStr} ${dim(fTime)} ${dim("1w:")}${wStr} ${dim(wTime)}${staleMarker}`;
398
+ const quotaSection = `${dim("5h:")}${fStr} ${dim(fTime)} ${dim("1w:")}${wStr} ${dim(wTime)}`;
403
399
  const warning = ctxView.warningTag
404
400
  ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
405
401
  : "";
@@ -408,7 +404,7 @@ export function getClaudeRows(
408
404
  }
409
405
 
410
406
  // full tier (>= 120 cols)
411
- const quotaSection = `${dim("5h:")}${fBar}${fStr} ${dim(fTime)} ${dim("1w:")}${wBar}${wStr} ${dim(wTime)}${staleMarker}`;
407
+ const quotaSection = `${dim("5h:")}${fBar}${fStr} ${dim(fTime)} ${dim("1w:")}${wBar}${wStr} ${dim(wTime)}`;
412
408
  const warning = ctxView.warningTag
413
409
  ? ` ${dim("|")} ${yellow(ctxView.warningTag)}`
414
410
  : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.0",
3
+ "version": "10.9.2",
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": {
@@ -52,12 +52,12 @@ function resolveHubTarget() {
52
52
 
53
53
  async function isHubHealthy(host, port) {
54
54
  try {
55
- const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
55
+ const res = await fetch(`${buildHubBaseUrl(host, port)}/health`, {
56
56
  signal: AbortSignal.timeout(1000),
57
57
  });
58
58
  if (!res.ok) return false;
59
59
  const data = await res.json();
60
- return data?.hub?.state === "healthy";
60
+ return data?.ok === true;
61
61
  } catch {
62
62
  return false;
63
63
  }
@@ -69,25 +69,13 @@ function startHubDetached(port) {
69
69
 
70
70
  try {
71
71
  const env = { ...process.env, TFX_HUB_PORT: String(port) };
72
- if (process.platform === "win32") {
73
- const child = spawn(
74
- "cmd.exe",
75
- ["/c", "start", "/b", "", process.execPath, serverPath],
76
- {
77
- env,
78
- stdio: "ignore",
79
- windowsHide: true,
80
- },
81
- );
82
- child.unref();
83
- } else {
84
- const child = spawn(process.execPath, [serverPath], {
85
- env,
86
- detached: true,
87
- stdio: "ignore",
88
- });
89
- child.unref();
90
- }
72
+ const child = spawn(process.execPath, [serverPath], {
73
+ env,
74
+ detached: true,
75
+ stdio: "ignore",
76
+ windowsHide: true,
77
+ });
78
+ child.unref();
91
79
  return true;
92
80
  } catch {
93
81
  return false;
@@ -114,13 +102,13 @@ export async function run(stdinData) {
114
102
 
115
103
  const started = startHubDetached(port);
116
104
  if (!started) {
117
- return { code: 0, stdout: "", stderr: "[hub-ensure] hub 시작 실패" };
105
+ return { code: 1, stdout: "", stderr: "[hub-ensure] hub 시작 실패" };
118
106
  }
119
107
 
120
- const ready = await waitForHubReady(host, port, 3000);
108
+ const ready = await waitForHubReady(host, port, 5000);
121
109
  return {
122
- code: 0,
123
- stdout: ready ? "hub: ok" : "hub: starting",
110
+ code: ready ? 0 : 2,
111
+ stdout: ready ? "hub: ok" : "hub: starting (timeout)",
124
112
  stderr: "",
125
113
  };
126
114
  }
@@ -920,7 +920,7 @@ route_agent() {
920
920
  TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
921
921
  TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
922
922
  TFX_VERIFIER_OVERRIDE="${TFX_VERIFIER_OVERRIDE:-auto}"
923
- TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-exec}"
923
+ TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
924
924
  # Preflight 캐시 일괄 로드 — CLI/Hub 가용성 + Codex 요금제를 환경변수로 내보냄
925
925
  # 하위 프로세스(스킬 포함)가 TFX_CODEX_OK, TFX_GEMINI_OK, TFX_HUB_OK로 즉시 참조 가능
926
926
  if [[ -z "${TFX_PREFLIGHT_LOADED:-}" ]]; then
@@ -93,21 +93,22 @@ dispatch 시 해당 스킬을 Skill 도구로 호출하고 **이 워크플로우
93
93
  > 2. **비용**: Codex 우선 → Gemini → Claude 최후 수단. `claude` 선택 전 "Codex로 가능한가?" 재확인.
94
94
  > 3. **DAG**: SEQUENTIAL/DAG이면 레벨 기반 순차 실행. `.omc/context/{sid}/` 생성, context_output 저장, 실패 시 후속 SKIP.
95
95
  > 4. **트리아지**: Codex `exec --full-auto` 분류 + Opus 인라인 분해. Agent 스폰 금지.
96
- > 5. **thorough**: `-t`/`--thorough` 파이프라인 init 필수. 커맨드 숏컷은 항상 quick.
96
+ > 5. **thorough 기본**: `--thorough`가 기본. Opus가 규모(S/M)·커맨드 숏컷 판단 자동 경량화 가능. `--quick`은 명시적 옵트아웃.
97
97
  > 6. **직접 수정 금지**: implement/review/analyze 등 커맨드 숏컷 실행 시 절대로 Edit/Write 도구로 직접 코드를 수정하지 마라. 반드시 Bash(tfx-route.sh)를 통해 Codex/Gemini에 위임하라. 작업이 아무리 사소해도 예외 없음.
98
98
 
99
99
  ## 모드
100
100
 
101
101
  | 입력 형식 | 모드 | 트리아지 |
102
102
  |-----------|------|----------|
103
- | `/implement JWT 추가` | 커맨드 숏컷 (quick) | 없음 (즉시 실행) |
104
- | `/tfx-auto "리팩터링 + UI"` | 자동 (quick) | Codex 분류 → Opus 분해 |
105
- | `/tfx-auto -t "리팩터링 + UI"` | 자동 (thorough) | Codex 분류 → Opus 분해 Pipeline |
106
- | `/tfx-auto --thorough "리팩터링"` | 자동 (thorough) | `-t` 동일 |
107
- | `/tfx-auto 3:codex "리뷰"` | 수동 (quick) | Opus 분해만 |
103
+ | `/implement JWT 추가` | 커맨드 숏컷 (thorough) | Opus 판단 규모 S면 자동 경량화 |
104
+ | `/tfx-auto "리팩터링 + UI"` | 자동 (thorough) | Codex 분류 → Opus 분해 → Pipeline |
105
+ | `/tfx-auto -q "빠르게 수정"` | 자동 (quick) | Opus 분해만, plan/verify 생략 |
106
+ | `/tfx-auto --quick "빠르게"` | 자동 (quick) | `-q` 동일 |
107
+ | `/tfx-auto 3:codex "리뷰"` | 수동 (thorough) | Opus 분해 + Pipeline |
108
108
 
109
- > **tfx-auto는 `--quick`이 기본.** 커맨드 숏컷·단일 실행에서 plan/verify 오버헤드가 불필요하기 때문.
110
- > 멀티 태스크 tfx-multi로 전환되면 tfx-multi의 기본값(`--thorough`)이 적용된다.
109
+ > **tfx-auto는 `--thorough`가 기본.** 모든 작업에 plan/verify 파이프라인을 적용한다.
110
+ > Opus가 규모(S)·단순 커맨드 숏컷으로 판단하면 자동 경량화(plan/verify 생략)한다.
111
+ > 명시적 `--quick`/`-q`로 강제 경량화 가능.
111
112
 
112
113
  ## 커맨드 숏컷
113
114
 
@@ -168,16 +169,16 @@ dispatch 시 해당 스킬을 Skill 도구로 호출하고 **이 워크플로우
168
169
 
169
170
  **수동 모드 (`N:agent_type`):** Codex 분류 건너뜀 → Opus가 N개 서브태스크 분해. N > 10 거부.
170
171
 
171
- ## --thorough 모드
172
+ ## 파이프라인 (기본: thorough)
172
173
 
173
- `-t` 또는 `--thorough` 플래그파이프라인 기반 실행. 커맨드 숏컷에서는 무시된다.
174
+ `--thorough`가 기본. `--quick`/`-q` 명시경량화. Opus가 규모 S·단순 숏컷으로 판단 시 자동 경량화.
174
175
 
175
176
  ```
176
177
  분기점은 "실행 전략"이지 "계획"이 아님:
177
178
 
178
179
  TRIAGE
179
180
 
180
- ├─ [thorough] → PIPELINE INIT(plan) → PLAN → PRD → [APPROVAL]
181
+ ├─ [기본/thorough] → PIPELINE INIT(plan) → PLAN → PRD → [APPROVAL]
181
182
  │ │
182
183
  │ ┌───────────────┤
183
184
  │ │ │
@@ -189,8 +190,10 @@ TRIAGE
189
190
  │ │
190
191
  │ VERIFY → FIX loop → COMPLETE
191
192
 
192
- └─ [quick] → [1 task] → fire-and-forget
193
- [2+ tasks] → TEAM EXEC → COLLECT → CLEANUP
193
+ ├─ [Opus 자동 경량화] → 규모 S + 단일 파일 → fire-and-forget (plan/verify 생략)
194
+
195
+ └─ [--quick 명시] → [1 task] → fire-and-forget
196
+ [2+ tasks] → TEAM EXEC → COLLECT → CLEANUP
194
197
  ```
195
198
 
196
199
  ### 단일 태스크 thorough
@@ -216,10 +219,11 @@ Plan/PRD/Approval은 tfx-auto에서 실행, 그 후 tfx-multi Phase 3로 전환.
216
219
 
217
220
  | 조건 | 실행 경로 | 엔진 |
218
221
  |------|----------|------|
219
- | 1개 + quick | tfx-auto 직접 실행 (fire-and-forget) | tfx-route.sh |
220
- | 1개 + thorough | tfx-auto 직접 실행 + verify/fix loop | tfx-route.sh |
221
- | 2개+ + quick | **headless 직접 실행** (WT 자동 팝업) | headless.mjs |
222
- | 2개+ + thorough | Plan/PRD/Approval 후 → headless + verify/fix | headless.mjs |
222
+ | 1개 (기본 thorough) | tfx-auto 직접 실행 + verify/fix loop | tfx-route.sh |
223
+ | 1개 + Opus 자동 경량화 | tfx-auto 직접 실행 (fire-and-forget) | tfx-route.sh |
224
+ | 1개 + `--quick` 명시 | tfx-auto 직접 실행 (fire-and-forget) | tfx-route.sh |
225
+ | 2개+ (기본 thorough) | Plan/PRD/Approval 후 → headless + verify/fix | headless.mjs |
226
+ | 2개+ + `--quick` 명시 | **headless 직접 실행** (WT 자동 팝업) | headless.mjs |
223
227
  | psmux 미설치 fallback | Native Teams (Agent slim wrapper) | native.mjs |
224
228
 
225
229
  > **MANDATORY: 2개+ 서브태스크 시 headless 엔진 필수**
@@ -229,19 +233,19 @@ Plan/PRD/Approval은 tfx-auto에서 실행, 그 후 tfx-multi Phase 3로 전환.
229
233
  **전환 방법:**
230
234
 
231
235
  ```
232
- thorough = args에 -t 또는 --thorough 포함
236
+ quick = args에 -q 또는 --quick 명시, 또는 Opus 자동 경량화 판단
233
237
 
234
238
  if subtasks.length >= 2:
235
239
  if psmux 설치됨:
236
240
  → Bash("tfx multi --teammate-mode headless --auto-attach --dashboard --assign 'cli:prompt:role' ...")
237
- → if thorough: verify → fix loop
241
+ → if !quick: verify → fix loop
238
242
  else:
239
243
  → fallback: tfx-multi Phase 3 Native Teams (Agent slim wrapper)
240
244
  else:
241
- if thorough:
242
- Pipeline init → Plan → PRD → Approval → 직접 실행 → Verify → Fix loop
245
+ if quick:
246
+ tfx-auto 직접 실행 (fire-and-forget)
243
247
  else:
244
- tfx-auto 직접 실행 (아래)
248
+ Pipeline init → Plan → PRD → Approval → 직접 실행 → Verify → Fix loop
245
249
  ```
246
250
 
247
251
  ## 실행
@@ -1,70 +1,124 @@
1
1
  ---
2
2
  name: tfx-hub
3
3
  description: >
4
- tfx-hub MCP 메시지 버스 관리. CLI 에이전트 실시간 통신 허브를 시작/중지/상태확인하고,
5
- hub 도메인의 자유형 작업도 처리합니다.
4
+ tfx-hub MCP 메시지 버스 관리. AskUserQuestion 기반 인터랙티브 UI로
5
+ 허브 시작/중지/상태확인, MCP 서버 관리, 에이전트 조회, 파이프라인 조회를 수행합니다.
6
6
  Use when: hub, 허브, 메시지 버스, message bus, 브릿지, bridge, MCP 서버 관리, 에이전트 통신
7
7
  triggers:
8
8
  - tfx-hub
9
- argument-hint: "<start|stop|status|자유형 작업 설명>"
9
+ argument-hint: "<start|stop|status|mcp|자유형 작업 설명>"
10
10
  ---
11
11
 
12
- # tfx-hub — MCP 메시지 버스 관리 + 개방형 작업
12
+ # tfx-hub — MCP 메시지 버스 관리
13
13
 
14
- > **ARGUMENTS 처리**: 이 스킬이 `ARGUMENTS: <값>`과 함께 호출되면, 해당 값을 사용자 입력으로 취급하여
15
- > 워크플로우의 단계 입력으로 사용한다. ARGUMENTS가 비어있거나 없으면 기존 절차대로 사용자에게 입력을 요청한다.
16
-
17
-
18
- > **인프라**: 다른 스킬이 내부적으로 사용. 직접 호출할 필요 없음.
19
- > CLI 에이전트(Codex/Gemini/Claude) 간 실시간 메시지 허브를 관리합니다.
20
- > **커맨드 매칭 + fallthrough**: start/stop/status에 매칭되면 즉시 실행,
21
- > 매칭 안 되면 **hub 도메인 컨텍스트를 활용한 범용 작업**으로 처리합니다.
14
+ > **ARGUMENTS 처리**: `ARGUMENTS: <값>`과 함께 호출되면 해당 값을 입력으로 사용한다.
15
+ > start/stop/status/mcp에 매칭되면 즉시 실행, 나머지는 메인 메뉴를 표시한다.
22
16
 
23
17
  ## 입력 해석 규칙
24
18
 
25
19
  ```
26
- /tfx-hub start 커맨드 매칭 허브 시작
27
- /tfx-hub stop 커맨드 매칭 허브 중지
28
- /tfx-hub status 커맨드 매칭 상태 확인
29
- /tfx-hub 테스트해줘 fallthrough hub 관련 범용 작업으로 처리
30
- /tfx-hub 문서 저장해 fallthrough hub 관련 범용 작업으로 처리
31
- /tfx-hub 브릿지 분석해 → fallthrough hub 관련 범용 작업으로 처리
20
+ /tfx-hub start 즉시 실행: 허브 시작
21
+ /tfx-hub stop 즉시 실행: 허브 중지
22
+ /tfx-hub status 즉시 실행: 상태 확인
23
+ /tfx-hub mcp 즉시 실행: MCP 서버 목록
24
+ /tfx-hub 메인 메뉴 표시
25
+ /tfx-hub 뭔가 → fallthrough: hub 도메인 범용 작업
32
26
  ```
33
27
 
34
- **fallthrough 규칙**: 인자가 start/stop/status/--port 등 커맨드 키워드에 매칭되지 않으면,
35
- 사용자의 입력을 **hub/브릿지/메시지버스 도메인의 자유형 작업**으로 해석한다.
28
+ ## 워크플로우
29
+
30
+ ### Step 0: 허브 상태 사전 확인
31
+
32
+ 메뉴 표시 전 허브 실행 상태를 먼저 확인한다:
36
33
 
37
- fallthrough 라우팅:
38
34
  ```bash
39
- # tfx-route.sh 경유 (권장)
40
- Bash("bash ~/.claude/scripts/tfx-route.sh {에이전트} '{hub 컨텍스트 + 작업}' {mcp_profile}")
35
+ Bash("curl -sf http://127.0.0.1:27888/status 2>/dev/null | node -e \"const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(JSON.stringify({running:true, uptime_ms:d.hub.uptime_ms, sessions:d.sessions, queues:d.queues, assigns:d.assigns}))\" 2>/dev/null || echo '{\"running\":false}'")
36
+ ```
41
37
 
42
- # codex 직접 호출 시 — 반드시 exec 서브커맨드 포함
43
- Bash("codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check '{작업}'")
44
- Bash("codex --profile gpt54_xhigh exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check '{작업}'")
45
- # ↑ --profile은 exec 앞에, --skip-git-repo-check은 exec 뒤에
38
+ 결과를 `hubState` 변수로 저장한다.
39
+
40
+ ### Step 1: 메인 메뉴 (AskUserQuestion)
46
41
 
47
- # Claude 네이티브 (탐색/검증)
48
- Agent(subagent_type="oh-my-claudecode:explore", prompt="{작업}")
49
42
  ```
43
+ question: "tfx-hub 관리 — 어떤 작업을 수행하시겠습니까?"
44
+ header: "tfx-hub {hubState.running ? '● 실행 중' : '○ 중지됨'} | 세션: {sessions} | 큐: urgent {queues.urgent_depth} / normal {queues.normal_depth} / DLQ {queues.dlq_depth}"
45
+ options:
46
+ - label: "허브 상태 보기"
47
+ description: "상세 상태 — 에이전트, 큐, 파이프라인, assign 현황"
48
+ - label: "허브 시작"
49
+ description: "MCP 서버를 :27888에서 시작"
50
+ - label: "허브 중지"
51
+ description: "실행 중인 허브 프로세스 종료"
52
+ - label: "MCP 서버 관리"
53
+ description: "등록된 MCP 서버 목록 조회, 추가, 제거"
54
+ - label: "파이프라인 조회"
55
+ description: "활성 파이프라인 목록 + 상태"
56
+ - label: "Assign 작업 조회"
57
+ description: "비동기 작업 목록 + 상태"
58
+ - label: "DLQ 관리"
59
+ description: "Dead Letter Queue 조회 + 재시도/삭제"
60
+ ```
61
+
62
+ 허브가 중지됨 상태라면 "허브 시작" 외 항목 선택 시 "허브가 실행 중이 아닙니다. 먼저 시작하시겠습니까?" 확인을 표시한다.
50
63
 
51
- ## 커맨드
64
+ ### Step 2: 선택에 따른 분기
52
65
 
53
- ### start 허브 시작
66
+ #### "허브 상태 보기"
67
+
68
+ ```bash
69
+ Bash("curl -s http://127.0.0.1:27888/status 2>/dev/null || echo '{\"error\":\"hub 미실행\"}'")
70
+ ```
71
+
72
+ 결과를 파싱하여 표시:
73
+
74
+ ```markdown
75
+ ## Hub Status
76
+
77
+ | 항목 | 값 |
78
+ |------|-----|
79
+ | 상태 | ● healthy |
80
+ | Uptime | 22m 3s |
81
+ | PID | 24504 |
82
+ | 포트 | 27888 |
83
+ | 인증 | localhost-only |
84
+ | 세션 | 0 |
85
+
86
+ ### 큐
87
+ | urgent | normal | DLQ |
88
+ |--------|--------|-----|
89
+ | 0 | 0 | 10 |
90
+
91
+ ### Assign
92
+ | queued | running | failed | timed_out |
93
+ |--------|---------|--------|-----------|
94
+ | 0 | 0 | 0 | 1 |
95
+ ```
96
+
97
+ 표시 후 메인 메뉴로 돌아갈지 AskUserQuestion:
98
+ ```
99
+ question: "추가 작업이 있으십니까?"
100
+ options:
101
+ - label: "메인 메뉴로"
102
+ - label: "종료"
103
+ ```
104
+
105
+ #### "허브 시작"
54
106
 
55
107
  ```bash
56
108
  Bash("node hub/server.mjs", run_in_background=true)
57
109
  ```
58
110
 
59
- - Streamable HTTP MCP 서버를 `http://127.0.0.1:27888/mcp` 에서 시작
60
- - SQLite WAL DB: `~/.claude/cache/tfx-hub/state.db`
61
- - PID 파일: `~/.claude/cache/tfx-hub/hub.pid`
62
- - 환경변수: `TFX_HUB_PORT` (포트), `TFX_HUB_DB` (DB 경로)
111
+ 시작 2초 대기하여 상태 확인:
112
+ ```bash
113
+ Bash("sleep 2 && curl -sf http://127.0.0.1:27888/status >/dev/null 2>&1 && echo 'OK' || echo 'FAIL'")
114
+ ```
115
+
116
+ - OK → "허브가 시작되었습니다. http://127.0.0.1:27888"
117
+ - FAIL → "허브 시작에 실패했습니다. `node hub/server.mjs`를 직접 실행해 보세요."
63
118
 
64
- ### stop — 허브 중지
119
+ #### "허브 중지"
65
120
 
66
121
  ```bash
67
- # PID 파일에서 프로세스 ID 읽어서 종료
68
122
  Bash("node -e \"
69
123
  const fs = require('fs');
70
124
  const path = require('path');
@@ -79,31 +133,179 @@ Bash("node -e \"
79
133
  \"")
80
134
  ```
81
135
 
82
- ### status 상태 확인
136
+ #### "MCP 서버 관리"
137
+
138
+ 먼저 현재 등록된 MCP 서버 목록을 수집:
83
139
 
84
140
  ```bash
85
- # HTTP 상태 엔드포인트 조회
86
- Bash("curl -s http://127.0.0.1:27888/status 2>/dev/null || echo '{\"error\":\"hub 미실행\"}'")
141
+ Bash("claude mcp list 2>/dev/null")
142
+ ```
143
+
144
+ 결과를 파싱하여 테이블로 표시:
145
+
146
+ ```markdown
147
+ ## 등록된 MCP 서버
148
+
149
+ | # | 이름 | 상태 | 명령어 |
150
+ |---|------|------|--------|
151
+ | 1 | context7 | ✓ Connected | cmd /c npx -y @upstash/context7-mcp@latest |
152
+ | 2 | exa | ✓ Connected | cmd /c npx -y exa-mcp-server |
153
+ | 3 | powerpoint | ✓ Connected | uvx ppt-mcp |
154
+ | ... | ... | ... | ... |
155
+ ```
156
+
157
+ 그 후 AskUserQuestion:
158
+ ```
159
+ question: "MCP 서버 관리 — 어떤 작업을 하시겠습니까?"
160
+ header: "MCP Servers ({connected}개 연결 / {failed}개 실패 / {disabled}개 비활성)"
161
+ options:
162
+ - label: "서버 제거"
163
+ description: "등록된 서버를 선택하여 제거"
164
+ - label: "서버 추가"
165
+ description: "새 MCP 서버 등록"
166
+ - label: "실패한 서버 재시작"
167
+ description: "연결 실패 서버를 재시작 시도"
168
+ - label: "뒤로"
169
+ description: "메인 메뉴로 돌아가기"
170
+ ```
171
+
172
+ ##### "서버 제거" 선택 시
173
+
174
+ AskUserQuestion (multiSelect):
175
+ ```
176
+ question: "제거할 서버를 선택하세요"
177
+ header: "MCP 서버 제거"
178
+ options:
179
+ (등록된 서버를 각각 옵션으로 나열)
180
+ - label: "powerpoint"
181
+ description: "[local] uvx ppt-mcp — ✓ Connected"
182
+ - label: "stability-ai"
183
+ description: "[local] npx -y mcp-server-stability-ai — ✘ Failed"
184
+ ...
185
+ multiSelect: true
186
+ ```
187
+
188
+ 선택된 서버들을 제거:
189
+ ```bash
190
+ Bash("claude mcp remove '{서버명}' -s {scope}")
191
+ ```
192
+
193
+ 제거 후 결과 표시.
194
+
195
+ ##### "서버 추가" 선택 시
196
+
197
+ AskUserQuestion:
198
+ ```
199
+ question: "추가할 서버 유형을 선택하세요"
200
+ header: "MCP 서버 추가"
201
+ options:
202
+ - label: "stdio (npx/uvx)"
203
+ description: "npx, uvx 등 stdio 기반 서버"
204
+ - label: "SSE/HTTP"
205
+ description: "URL 기반 원격 서버"
206
+ ```
207
+
208
+ stdio 선택 시 AskUserQuestion:
209
+ ```
210
+ question: "서버 이름과 명령어를 입력하세요 (예: myserver -- cmd /c npx -y my-mcp-server)"
211
+ header: "stdio 서버 추가"
212
+ ```
213
+
214
+ 입력값을 파싱하여:
215
+ ```bash
216
+ Bash("claude mcp add '{name}' -s local -- {command}")
217
+ ```
218
+
219
+ SSE/HTTP 선택 시 AskUserQuestion:
220
+ ```
221
+ question: "서버 이름과 URL을 입력하세요 (예: myserver http://localhost:8080/mcp)"
222
+ header: "HTTP 서버 추가"
223
+ ```
224
+
225
+ 입력값을 파싱하여:
226
+ ```bash
227
+ Bash("claude mcp add --transport http '{name}' '{url}' -s local")
228
+ ```
229
+
230
+ ##### "실패한 서버 재시작" 선택 시
231
+
232
+ `claude mcp list` 결과에서 Failed 서버만 필터:
233
+
234
+ ```
235
+ question: "재시작할 서버를 선택하세요"
236
+ header: "실패한 MCP 서버"
237
+ options:
238
+ (Failed 서버만 나열)
239
+ multiSelect: true
240
+ ```
241
+
242
+ 선택된 서버를 제거 후 재등록하여 재시작.
243
+
244
+ #### "파이프라인 조회"
245
+
246
+ ```bash
247
+ Bash("curl -s http://127.0.0.1:27888/bridge/pipeline/list -X POST -H 'Content-Type: application/json' -d '{}' 2>/dev/null || echo '{\"error\":\"hub 미실행\"}'")
248
+ ```
249
+
250
+ 결과를 테이블로 표시.
251
+
252
+ #### "Assign 작업 조회"
253
+
254
+ ```bash
255
+ Bash("curl -s http://127.0.0.1:27888/bridge/assign/status -X POST -H 'Content-Type: application/json' -d '{\"list\":true}' 2>/dev/null || echo '{\"error\":\"hub 미실행\"}'")
256
+ ```
257
+
258
+ 결과를 테이블로 표시. 실패 작업이 있으면 재시도 옵션 제공.
259
+
260
+ #### "DLQ 관리"
261
+
262
+ ```bash
263
+ Bash("curl -s http://127.0.0.1:27888/status 2>/dev/null | node -e \"const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log('DLQ depth: '+d.queues.dlq_depth)\"")
87
264
  ```
88
265
 
89
- ## CLI 등록 방법
266
+ DLQ가 비어있으면 "DLQ가 비어있습니다." 표시.
267
+ 내용이 있으면 AskUserQuestion:
268
+ ```
269
+ question: "DLQ 처리 — 어떻게 하시겠습니까?"
270
+ header: "Dead Letter Queue ({count}건)"
271
+ options:
272
+ - label: "전체 조회"
273
+ description: "DLQ 메시지 목록 표시"
274
+ - label: "전체 재시도"
275
+ description: "모든 DLQ 메시지를 다시 큐에 넣기"
276
+ - label: "전체 삭제"
277
+ description: "DLQ 비우기"
278
+ - label: "뒤로"
279
+ ```
280
+
281
+ ## fallthrough 라우팅
282
+
283
+ 메인 메뉴 항목에 매칭되지 않는 자유형 입력은 hub 도메인 컨텍스트 범용 작업으로 처리:
284
+
285
+ ```bash
286
+ # tfx-route.sh 경유 (권장)
287
+ Bash("bash ~/.claude/scripts/tfx-route.sh {에이전트} '{hub 컨텍스트 + 작업}' {mcp_profile}")
288
+
289
+ # Claude 네이티브 (탐색/검증)
290
+ Agent(subagent_type="oh-my-claudecode:explore", prompt="{작업}")
291
+ ```
292
+
293
+ ## CLI 등록 방법
90
294
 
91
295
  허브 시작 후 각 CLI에 MCP 서버로 등록:
92
296
 
93
297
  ```bash
94
- # Codex (수동 opt-in 예시)
95
- # triflux는 config.json을 자동 관리하며, standalone Codex 노이즈 방지를 위해
96
- # 사전 등록은 disabled로 두고 `tfx hub start` 이후에만 enabled로 전환한다.
298
+ # Claude
299
+ claude mcp add --transport http tfx-hub http://127.0.0.1:27888/mcp
300
+
301
+ # Codex
97
302
  codex mcp add tfx-hub --url http://127.0.0.1:27888/mcp
98
303
 
99
304
  # Gemini (settings.json)
100
305
  # mcpServers.tfx-hub.url = "http://127.0.0.1:27888/mcp"
101
-
102
- # Claude
103
- claude mcp add --transport http tfx-hub http://127.0.0.1:27888/mcp
104
306
  ```
105
307
 
106
- ## MCP 도구 (20개)
308
+ ## MCP 도구 레퍼런스 (20개)
107
309
 
108
310
  ### Core — 기본 통신
109
311
 
@@ -150,14 +352,25 @@ claude mcp add --transport http tfx-hub http://127.0.0.1:27888/mcp
150
352
  | `request_human_input` | 사용자 입력 요청 (CAPTCHA/승인/자격증명/선택/텍스트) |
151
353
  | `submit_human_input` | 사용자 입력 응답 (accept/decline/cancel) |
152
354
 
153
- ## 브릿지 REST 엔드포인트 (4개)
355
+ ## CLI 대응
154
356
 
155
- | 엔드포인트 | 설명 |
156
- |-----------|------|
157
- | `POST /bridge/register` | 에이전트 등록 (프로세스 수명 기반 lease) |
158
- | `POST /bridge/result` | 결과 발행 (topic fanout) |
159
- | `POST /bridge/context` | 선행 컨텍스트 폴링 (auto_ack) |
160
- | `POST /bridge/deregister` | 에이전트 offline 마킹 |
357
+ | 스킬 UI | CLI 명령 |
358
+ |---------|---------|
359
+ | 허브 상태 보기 | `curl http://127.0.0.1:27888/status` |
360
+ | 허브 시작 | `node hub/server.mjs` |
361
+ | 허브 중지 | PID 파일에서 kill |
362
+ | MCP 서버 목록 | `claude mcp list` |
363
+ | MCP 서버 추가 | `claude mcp add ...` |
364
+ | MCP 서버 제거 | `claude mcp remove ...` |
365
+
366
+ ## 에러 처리
367
+
368
+ | 상황 | 처리 |
369
+ |------|------|
370
+ | 허브 미실행 상태에서 조회 | "허브가 실행 중이 아닙니다. 시작하시겠습니까?" AskUserQuestion |
371
+ | curl 타임아웃 | "허브가 응답하지 않습니다. PID 파일을 확인하세요." |
372
+ | MCP 서버 추가 실패 | 에러 메시지 표시 + "다시 시도" 옵션 |
373
+ | PID 파일 없음 | "허브가 실행 중이 아닙니다." |
161
374
 
162
375
  ## 프로젝트 구조
163
376
 
@@ -170,41 +383,16 @@ hub/
170
383
  ├── hitl.mjs # Human-in-the-Loop 매니저
171
384
  ├── bridge.mjs # tfx-route.sh ↔ hub 브릿지 CLI
172
385
  ├── schema.sql # DB 스키마
173
- ├── paths.mjs # 경로 상수 (PID 파일, DB 경로 등)
174
- ├── pipe.mjs # Named Pipe 서버 (push 구독 채널)
175
- ├── assign-callbacks.mjs # assign job 콜백 처리
176
- ├── intent.mjs # 인텐트 파싱
177
- ├── reflexion.mjs # reflexion 루프
178
- ├── research.mjs # 리서치 프록시
179
- ├── token-mode.mjs # 토큰 모드 관리
386
+ ├── paths.mjs # 경로 상수
387
+ ├── pipe.mjs # Named Pipe 서버
388
+ ├── assign-callbacks.mjs # assign job 콜백
180
389
  ├── pipeline/ # 파이프라인 엔진
181
- │ ├── index.mjs # createPipeline() 팩토리
182
- │ ├── state.mjs # 파이프라인 상태 CRUD
183
- │ ├── transitions.mjs # 전이 규칙
184
- │ └── gates/ # HITL 게이트 (selfcheck, confidence)
185
390
  ├── delegator/ # 작업 위임 레이어
186
- │ ├── index.mjs
187
- │ ├── service.mjs
188
- │ ├── contracts.mjs
189
- │ └── tool-definitions.mjs
190
391
  ├── team/ # Claude Native Teams 통합
191
- │ ├── nativeProxy.mjs # Teams MCP 프록시
192
- │ ├── orchestrator.mjs # 팀 오케스트레이터
193
- │ ├── session.mjs # 세션 관리
194
- │ ├── dashboard.mjs # TUI 대시보드
195
- │ ├── tui.mjs # TUI 렌더러
196
- │ └── cli/ # tfx team CLI 커맨드
197
392
  ├── workers/ # CLI 워커 어댑터
198
- ├── factory.mjs
199
- │ ├── claude-worker.mjs
200
- │ ├── codex-mcp.mjs
201
- │ ├── gemini-worker.mjs
202
- │ └── delegator-mcp.mjs
203
- ├── middleware/ # 요청 미들웨어
204
- │ └── request-logger.mjs
393
+ ├── middleware/ # 요청 미들웨어
205
394
  ├── quality/ # 품질 검사
206
- └── deslop.mjs
207
- └── public/ # 정적 자산 (대시보드 HTML, 트레이 아이콘)
395
+ └── public/ # 정적 자산
208
396
  ```
209
397
 
210
398
  ## 상태
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tfx-hub",
3
- "description": "tfx-hub MCP 메시지 버스 관리. CLI 에이전트 실시간 통신 허브를 시작/중지/상태확인하고, hub 도메인의 자유형 작업도 처리합니다. Use when: hub, 허브, 메시지 버스, message bus, 브릿지, bridge, MCP 서버 관리, 에이전트 통신",
3
+ "description": "tfx-hub MCP 메시지 버스 관리. AskUserQuestion 기반 인터랙티브 UI로 허브 시작/중지/상태확인, MCP 서버 관리, 에이전트 조회, 파이프라인 조회를 수행합니다. Use when: hub, 허브, 메시지 버스, message bus, 브릿지, bridge, MCP 서버 관리, 에이전트 통신",
4
4
  "triggers": ["tfx-hub"],
5
- "argument_hint": "<start|stop|status|자유형 작업 설명>"
5
+ "argument_hint": "<start|stop|status|mcp|자유형 작업 설명>"
6
6
  }