triflux 10.9.30 → 10.9.31

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 (55) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/bin/triflux.mjs +170 -20
  4. package/hooks/keyword-rules.json +7 -24
  5. package/hooks/session-start-fast.mjs +54 -12
  6. package/hub/cli-adapter-base.mjs +25 -5
  7. package/hub/lib/env-detect.mjs +36 -9
  8. package/hub/pipe.mjs +4 -1
  9. package/hub/platform.mjs +3 -1
  10. package/hub/server.mjs +120 -38
  11. package/hub/team/conductor.mjs +6 -5
  12. package/hub/team/dashboard-open.mjs +21 -8
  13. package/hub/team/git-preflight.mjs +9 -9
  14. package/hub/team/handoff.mjs +2 -1
  15. package/hub/team/headless.mjs +26 -9
  16. package/hub/team/notify.mjs +2 -2
  17. package/hub/team/process-cleanup.mjs +1 -1
  18. package/hub/team/psmux.mjs +84 -22
  19. package/hub/team/runtime-strategy.mjs +2 -2
  20. package/hub/team/swarm-cli.mjs +5 -7
  21. package/hub/team/swarm-hypervisor.mjs +12 -5
  22. package/hub/team/swarm-planner.mjs +3 -1
  23. package/hub/team/synapse-cli.mjs +19 -5
  24. package/hub/team/synapse-registry.mjs +29 -8
  25. package/hub/team/tui-core.mjs +8 -12
  26. package/hub/team/tui-lite.mjs +2 -2
  27. package/hub/team/tui-synapse.mjs +20 -10
  28. package/hub/team/tui.mjs +35 -20
  29. package/hub/team/worktree-lifecycle.mjs +3 -1
  30. package/hub/team/wt-manager.mjs +10 -2
  31. package/hub/workers/codex-app-server-worker.mjs +169 -170
  32. package/hub/workers/delegator-mcp.mjs +23 -14
  33. package/hub/workers/gemini-worker.mjs +6 -2
  34. package/hub/workers/lib/jsonrpc-stdio.mjs +19 -19
  35. package/package.json +1 -1
  36. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +7 -3
  37. package/scripts/claude-login-detect.mjs +24 -5
  38. package/scripts/config-audit.mjs +92 -22
  39. package/scripts/doctor-diagnose.mjs +64 -22
  40. package/scripts/headless-guard.mjs +6 -2
  41. package/scripts/hub-watchdog.mjs +1 -1
  42. package/scripts/keyword-detector.mjs +3 -1
  43. package/scripts/lib/cross-review-utils.mjs +6 -1
  44. package/scripts/lib/keyword-rules.mjs +1 -3
  45. package/scripts/lib/mcp-filter.mjs +2 -1
  46. package/scripts/release/lib.mjs +10 -4
  47. package/scripts/remote-spawn.mjs +13 -5
  48. package/scripts/session-stale-cleanup.mjs +19 -10
  49. package/scripts/setup.mjs +17 -13
  50. package/skills/tfx-auto/SKILL.md +35 -64
  51. package/skills/tfx-deep-analysis/SKILL.md +6 -1
  52. package/skills/tfx-doctor/SKILL.md +0 -21
  53. package/skills/tfx-hub/SKILL.md +85 -273
  54. package/skills/tfx-setup/SKILL.md +41 -0
  55. package/skills/tfx-setup/SKILL.md.tmpl +41 -0
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "triflux",
11
11
  "description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
12
- "version": "10.9.30",
12
+ "version": "10.9.31",
13
13
  "author": {
14
14
  "name": "tellang"
15
15
  },
@@ -30,5 +30,5 @@
30
30
  ]
31
31
  }
32
32
  ],
33
- "version": "10.9.30"
33
+ "version": "10.9.31"
34
34
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.30",
3
+ "version": "10.9.31",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "author": {
6
6
  "name": "tellang"
package/bin/triflux.mjs CHANGED
@@ -154,7 +154,8 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
154
154
  {
155
155
  name: "--diagnose",
156
156
  type: "boolean",
157
- description: "진단 번들(zip) 생성: spawn-trace + hook timing + system info",
157
+ description:
158
+ "진단 번들(zip) 생성: spawn-trace + hook timing + system info",
158
159
  },
159
160
  {
160
161
  name: "--json",
@@ -1229,10 +1230,13 @@ function cmdSetup(options = {}) {
1229
1230
  // ── tmux 기본 셸 확인 (macOS/Linux) ──
1230
1231
  if (process.platform !== "win32" && which("tmux")) {
1231
1232
  try {
1232
- const shellOut = execSync("tmux show-options -g default-shell 2>/dev/null", {
1233
- encoding: "utf8",
1234
- timeout: 3000,
1235
- }).trim();
1233
+ const shellOut = execSync(
1234
+ "tmux show-options -g default-shell 2>/dev/null",
1235
+ {
1236
+ encoding: "utf8",
1237
+ timeout: 3000,
1238
+ },
1239
+ ).trim();
1236
1240
  if (shellOut) {
1237
1241
  ok(`tmux 기본 셸: ${shellOut.split(/\s+/).pop() || "확인 완료"}`);
1238
1242
  }
@@ -1936,7 +1940,10 @@ async function cmdDoctor(options = {}) {
1936
1940
  }
1937
1941
  {
1938
1942
  const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
1939
- if (claudeGuide.skipped && claudeGuide.reason !== "global_sync_disabled")
1943
+ if (
1944
+ claudeGuide.skipped &&
1945
+ claudeGuide.reason !== "global_sync_disabled"
1946
+ )
1940
1947
  warn(`CLAUDE.md 라우팅 섹션 확인 실패: ${claudeGuide.reason}`);
1941
1948
  else if (
1942
1949
  claudeGuide.action === "created" ||
@@ -2504,6 +2511,86 @@ async function cmdDoctor(options = {}) {
2504
2511
  addDoctorCheck(report, { name: "stale-skills", status: "ok" });
2505
2512
  }
2506
2513
 
2514
+ // 8.5 Dev 의존성 (npm link 환경에서 node_modules 누락 감지 — Issue #101)
2515
+ section("Dev Dependencies");
2516
+ try {
2517
+ const pkgJsonPath = join(PKG_ROOT, "package.json");
2518
+ const nodeModulesPath = join(PKG_ROOT, "node_modules");
2519
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
2520
+ const runtimeDeps = Object.keys(pkgJson.dependencies || {});
2521
+ const isLinkedDev = (() => {
2522
+ try {
2523
+ const stat = statSync(nodeModulesPath);
2524
+ return stat.isDirectory();
2525
+ } catch {
2526
+ return false;
2527
+ }
2528
+ })();
2529
+ const missingDeps = [];
2530
+ if (!isLinkedDev) {
2531
+ // node_modules 전체 누락 — 전역 install 또는 미압축 릴리즈일 가능성
2532
+ if (runtimeDeps.length > 0) {
2533
+ missingDeps.push(...runtimeDeps);
2534
+ }
2535
+ } else {
2536
+ for (const dep of runtimeDeps) {
2537
+ const depPath = join(nodeModulesPath, ...dep.split("/"));
2538
+ if (!existsSync(depPath)) missingDeps.push(dep);
2539
+ }
2540
+ }
2541
+ if (missingDeps.length === 0) {
2542
+ ok(
2543
+ `의존성 ${runtimeDeps.length}개 설치됨${
2544
+ isLinkedDev ? ` ${DIM}(dev)${RESET}` : ""
2545
+ }`,
2546
+ );
2547
+ addDoctorCheck(report, {
2548
+ name: "dev-deps",
2549
+ status: "ok",
2550
+ total: runtimeDeps.length,
2551
+ linkedDev: isLinkedDev,
2552
+ });
2553
+ } else {
2554
+ const head = missingDeps.slice(0, 5);
2555
+ const tail = missingDeps.length > 5 ? `+${missingDeps.length - 5}` : "";
2556
+ warn(
2557
+ `누락 의존성 ${missingDeps.length}개: ${head.join(", ")}${tail ? ` ${tail}` : ""}`,
2558
+ );
2559
+ info("수정: tfx doctor --fix 또는 `npm install` (PKG_ROOT 내)");
2560
+ addDoctorCheck(report, {
2561
+ name: "dev-deps",
2562
+ status: "missing",
2563
+ missing: missingDeps,
2564
+ linkedDev: isLinkedDev,
2565
+ pkgRoot: PKG_ROOT,
2566
+ fix: "tfx doctor --fix",
2567
+ });
2568
+ issues++;
2569
+ if (fix) {
2570
+ // --fix 모드: npm install 실행 (Windows 호환 shell: true)
2571
+ info(`npm install 실행 중 (${PKG_ROOT})...`);
2572
+ try {
2573
+ const { execFileSync } = await import("node:child_process");
2574
+ execFileSync("npm", ["install", "--no-audit", "--no-fund"], {
2575
+ cwd: PKG_ROOT,
2576
+ stdio: "inherit",
2577
+ shell: process.platform === "win32",
2578
+ });
2579
+ ok("npm install 완료 — 의존성 복구됨");
2580
+ } catch (err) {
2581
+ warn(`npm install 실패: ${err?.message || err}`);
2582
+ }
2583
+ }
2584
+ }
2585
+ } catch (err) {
2586
+ warn(`dev 의존성 체크 실패: ${err?.message || err}`);
2587
+ addDoctorCheck(report, {
2588
+ name: "dev-deps",
2589
+ status: "error",
2590
+ error: String(err?.message || err),
2591
+ });
2592
+ }
2593
+
2507
2594
  // 9. 플러그인 등록
2508
2595
  section("Plugin");
2509
2596
  const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
@@ -4849,13 +4936,35 @@ async function cmdHub(args = [], options = {}) {
4849
4936
  });
4850
4937
  }
4851
4938
 
4939
+ // Issue #102: spawn stderr 를 임시 파일로 캡처해 실패 시 root cause 노출.
4940
+ // detached spawn 은 pipe 유지가 까다로우니 fd 리다이렉트로 접근.
4941
+ const { openSync: _openSync, closeSync: _closeSync } = await import(
4942
+ "node:fs"
4943
+ );
4944
+ const { tmpdir: _tmpdir } = await import("node:os");
4945
+ const startupErrPath = join(
4946
+ _tmpdir(),
4947
+ `tfx-hub-start-${Date.now()}-${process.pid}.err`,
4948
+ );
4949
+ let errFd;
4950
+ try {
4951
+ errFd = _openSync(startupErrPath, "w");
4952
+ } catch {
4953
+ errFd = undefined;
4954
+ }
4955
+
4852
4956
  const child = spawn(process.execPath, [serverPath], {
4853
4957
  env: { ...process.env, TFX_HUB_PORT: port },
4854
- stdio: "ignore",
4958
+ stdio: ["ignore", "ignore", errFd ?? "ignore"],
4855
4959
  detached: true,
4856
4960
  windowsHide: true,
4857
4961
  });
4858
4962
  child.unref();
4963
+ if (errFd !== undefined) {
4964
+ try {
4965
+ _closeSync(errFd);
4966
+ } catch {}
4967
+ }
4859
4968
 
4860
4969
  // PID 파일 확인 (최대 3초 대기, 100ms 폴링)
4861
4970
  let started = false;
@@ -4879,13 +4988,46 @@ async function cmdHub(args = [], options = {}) {
4879
4988
  console.log("");
4880
4989
  autoRegisterMcp(hubInfo.url, { codexEnabled: true });
4881
4990
  console.log("");
4991
+ // 성공했으면 임시 stderr 파일 정리
4992
+ try {
4993
+ unlinkSync(startupErrPath);
4994
+ } catch {}
4882
4995
  } else {
4883
- // 직접 포그라운드 모드로 안내
4884
- console.log(
4885
- `\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패 — 포그라운드로 실행:`,
4886
- );
4996
+ // Issue #102: 캡처된 stderr 에서 root cause 추출
4997
+ let rootCause = "";
4998
+ try {
4999
+ rootCause = readFileSync(startupErrPath, "utf8").trim();
5000
+ } catch {}
5001
+
5002
+ console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패`);
5003
+
5004
+ if (rootCause) {
5005
+ // 가장 유용한 에러 라인 강조 (ERR_*, Error:, throw)
5006
+ const highlight = rootCause
5007
+ .split(/\r?\n/)
5008
+ .find((line) => /ERR_[A-Z_]+|^Error:|cannot find/i.test(line));
5009
+ if (highlight) {
5010
+ console.log(` ${RED}▸ ${highlight.trim()}${RESET}`);
5011
+ }
5012
+ console.log(`\n ${DIM}전체 로그: ${startupErrPath}${RESET}`);
5013
+ // 원인별 실전 힌트
5014
+ if (/Cannot find package/i.test(rootCause)) {
5015
+ console.log(
5016
+ ` ${DIM}힌트: \`cd ${PKG_ROOT} && npm install\` 로 의존성 복구 (특히 \`npm link\` 환경).${RESET}`,
5017
+ );
5018
+ } else if (/EADDRINUSE/i.test(rootCause)) {
5019
+ console.log(
5020
+ ` ${DIM}힌트: 포트 ${port} 이 이미 사용 중. \`tfx hub stop\` 후 재시도.${RESET}`,
5021
+ );
5022
+ }
5023
+ } else {
5024
+ console.log(
5025
+ ` ${DIM}stderr 캡처 실패 — 아래 명령으로 포그라운드 실행해 원인 확인:${RESET}`,
5026
+ );
5027
+ }
5028
+
4887
5029
  console.log(
4888
- ` ${DIM}TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`,
5030
+ `\n ${DIM}포그라운드 실행: TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`,
4889
5031
  );
4890
5032
  }
4891
5033
  break;
@@ -5253,9 +5395,15 @@ async function main() {
5253
5395
  const auditScript = join(PKG_ROOT, "scripts", "config-audit.mjs");
5254
5396
  const auditArgs = JSON_OUTPUT ? ["--json"] : [];
5255
5397
  try {
5256
- const out = execFileSync(process.execPath, [auditScript, ...auditArgs], {
5257
- timeout: 15000, encoding: "utf8", windowsHide: true,
5258
- });
5398
+ const out = execFileSync(
5399
+ process.execPath,
5400
+ [auditScript, ...auditArgs],
5401
+ {
5402
+ timeout: 15000,
5403
+ encoding: "utf8",
5404
+ windowsHide: true,
5405
+ },
5406
+ );
5259
5407
  process.stdout.write(out);
5260
5408
  } catch (e) {
5261
5409
  process.stdout.write(e.stdout || "");
@@ -5268,8 +5416,12 @@ async function main() {
5268
5416
  const result = await diagnose({ json: JSON_OUTPUT });
5269
5417
  if (!JSON_OUTPUT) {
5270
5418
  if (result.ok) {
5271
- console.log(`\n ${GREEN_BRIGHT}✓${RESET} 진단 번들 생성: ${result.zipPath}`);
5272
- console.log(` spawn 이벤트: ${result.traceCount}건, 타이밍: ${result.hookTimingCount}건\n`);
5419
+ console.log(
5420
+ `\n ${GREEN_BRIGHT}✓${RESET} 진단 번들 생성: ${result.zipPath}`,
5421
+ );
5422
+ console.log(
5423
+ ` spawn 이벤트: ${result.traceCount}건, 훅 타이밍: ${result.hookTimingCount}건\n`,
5424
+ );
5273
5425
  } else {
5274
5426
  console.log(`\n ${RED}✗${RESET} 진단 실패: ${result.error}\n`);
5275
5427
  }
@@ -5397,9 +5549,7 @@ async function main() {
5397
5549
  return;
5398
5550
  }
5399
5551
  case "synapse": {
5400
- const { cmdSynapseStatus } = await import(
5401
- "../hub/team/synapse-cli.mjs"
5402
- );
5552
+ const { cmdSynapseStatus } = await import("../hub/team/synapse-cli.mjs");
5403
5553
  const sub = cmdArgs[0] || "status";
5404
5554
  if (sub !== "status") {
5405
5555
  throw createCliError(`synapse 서브커맨드 미지원: ${sub}`, {
@@ -11,12 +11,7 @@
11
11
  "skill": null,
12
12
  "action": "suppress_all",
13
13
  "priority": 0,
14
- "supersedes": [
15
- "tfx-multi",
16
- "tfx-unified",
17
- "tfx-codex",
18
- "tfx-gemini"
19
- ],
14
+ "supersedes": ["tfx-multi", "tfx-unified", "tfx-codex", "tfx-gemini"],
20
15
  "exclusive": true,
21
16
  "state": null,
22
17
  "mcp_route": null
@@ -62,9 +57,7 @@
62
57
  ],
63
58
  "skill": "tfx-swarm",
64
59
  "priority": 1,
65
- "supersedes": [
66
- "tfx-codex"
67
- ],
60
+ "supersedes": ["tfx-codex"],
68
61
  "exclusive": false,
69
62
  "state": null,
70
63
  "mcp_route": null
@@ -111,9 +104,7 @@
111
104
  ],
112
105
  "skill": "tfx-auto",
113
106
  "priority": 2,
114
- "supersedes": [
115
- "tfx-auto-codex"
116
- ],
107
+ "supersedes": ["tfx-auto-codex"],
117
108
  "exclusive": false,
118
109
  "state": null,
119
110
  "mcp_route": null
@@ -249,9 +240,7 @@
249
240
  ],
250
241
  "skill": "tfx-wt",
251
242
  "priority": 1,
252
- "supersedes": [
253
- "tfx-unified"
254
- ],
243
+ "supersedes": ["tfx-unified"],
255
244
  "exclusive": false,
256
245
  "state": null,
257
246
  "mcp_route": null
@@ -270,9 +259,7 @@
270
259
  ],
271
260
  "skill": "tfx-wt",
272
261
  "priority": 1,
273
- "supersedes": [
274
- "tfx-unified"
275
- ],
262
+ "supersedes": ["tfx-unified"],
276
263
  "exclusive": false,
277
264
  "state": null,
278
265
  "mcp_route": null
@@ -291,9 +278,7 @@
291
278
  ],
292
279
  "skill": "tfx-wt",
293
280
  "priority": 1,
294
- "supersedes": [
295
- "tfx-unified"
296
- ],
281
+ "supersedes": ["tfx-unified"],
297
282
  "exclusive": false,
298
283
  "state": null,
299
284
  "mcp_route": null
@@ -312,9 +297,7 @@
312
297
  ],
313
298
  "skill": "tfx-wt",
314
299
  "priority": 1,
315
- "supersedes": [
316
- "tfx-unified"
317
- ],
300
+ "supersedes": ["tfx-unified"],
318
301
  "exclusive": false,
319
302
  "state": null,
320
303
  "mcp_route": null
@@ -38,9 +38,15 @@ async function runBlocking(stdinData) {
38
38
  timings.push({ hook: "setup.critical", dur_ms: Math.round(dur) });
39
39
  if (result?.stdout) output.stdout += result.stdout + "\n";
40
40
  if (result?.stderr) output.stderr += result.stderr + "\n";
41
- log.info({ hook: "setup.critical", dur_ms: Math.round(dur) }, "hook.completed");
41
+ log.info(
42
+ { hook: "setup.critical", dur_ms: Math.round(dur) },
43
+ "hook.completed",
44
+ );
42
45
  } catch (err) {
43
- log.error({ hook: "setup.critical", err: String(err.message || err) }, "hook.failed");
46
+ log.error(
47
+ { hook: "setup.critical", err: String(err.message || err) },
48
+ "hook.failed",
49
+ );
44
50
  }
45
51
 
46
52
  // 2. mcp-safety-guard.run — EPERM 방지
@@ -50,9 +56,15 @@ async function runBlocking(stdinData) {
50
56
  guard.run();
51
57
  const dur = performance.now() - t0;
52
58
  timings.push({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) });
53
- log.info({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) }, "hook.completed");
59
+ log.info(
60
+ { hook: "mcp-safety-guard", dur_ms: Math.round(dur) },
61
+ "hook.completed",
62
+ );
54
63
  } catch (err) {
55
- log.error({ hook: "mcp-safety-guard", err: String(err.message || err) }, "hook.failed");
64
+ log.error(
65
+ { hook: "mcp-safety-guard", err: String(err.message || err) },
66
+ "hook.failed",
67
+ );
56
68
  }
57
69
 
58
70
  // 3. hub-ensure — Hub 필수 인프라, BLOCKING으로 실행
@@ -65,12 +77,21 @@ async function runBlocking(stdinData) {
65
77
  if (result?.stdout) output.stdout += result.stdout + "\n";
66
78
  if (result?.stderr) output.stderr += result.stderr + "\n";
67
79
  if (result?.code !== 0) {
68
- log.warn({ hook: "hub-ensure", dur_ms: Math.round(dur), code: result?.code }, "hook.warn");
80
+ log.warn(
81
+ { hook: "hub-ensure", dur_ms: Math.round(dur), code: result?.code },
82
+ "hook.warn",
83
+ );
69
84
  } else {
70
- log.info({ hook: "hub-ensure", dur_ms: Math.round(dur) }, "hook.completed");
85
+ log.info(
86
+ { hook: "hub-ensure", dur_ms: Math.round(dur) },
87
+ "hook.completed",
88
+ );
71
89
  }
72
90
  } catch (err) {
73
- log.error({ hook: "hub-ensure", err: String(err.message || err) }, "hook.failed");
91
+ log.error(
92
+ { hook: "hub-ensure", err: String(err.message || err) },
93
+ "hook.failed",
94
+ );
74
95
  }
75
96
 
76
97
  return { ...output, timings };
@@ -96,7 +117,9 @@ function runDeferred(stdinData) {
96
117
  const mod = await importMod(join(SCRIPTS, "claude-login-detect.mjs"));
97
118
  const result = mod.run?.();
98
119
  if (result?.changed) {
99
- return { stdout: `[claude-login] HUD 캐시 ${result.cleared}개 초기화됨\n` };
120
+ return {
121
+ stdout: `[claude-login] HUD 캐시 ${result.cleared}개 초기화됨\n`,
122
+ };
100
123
  }
101
124
  },
102
125
  },
@@ -118,14 +141,25 @@ function runDeferred(stdinData) {
118
141
 
119
142
  for (const task of tasks) {
120
143
  const t0 = performance.now();
121
- task.fn()
144
+ task
145
+ .fn()
122
146
  .then((result) => {
123
147
  const dur = performance.now() - t0;
124
- log.info({ hook: task.name, dur_ms: Math.round(dur), code: result?.code }, "deferred.completed");
148
+ log.info(
149
+ { hook: task.name, dur_ms: Math.round(dur), code: result?.code },
150
+ "deferred.completed",
151
+ );
125
152
  })
126
153
  .catch((err) => {
127
154
  const dur = performance.now() - t0;
128
- log.error({ hook: task.name, dur_ms: Math.round(dur), err: String(err.message || err) }, "deferred.failed");
155
+ log.error(
156
+ {
157
+ hook: task.name,
158
+ dur_ms: Math.round(dur),
159
+ err: String(err.message || err),
160
+ },
161
+ "deferred.failed",
162
+ );
129
163
  });
130
164
  }
131
165
  }
@@ -162,7 +196,15 @@ export async function execute(stdinData, externalHooks = []) {
162
196
  runBackground(stdinData);
163
197
 
164
198
  const totalDur = performance.now() - totalStart;
165
- log.info({ total_ms: Math.round(totalDur), blocking_count: 3, deferred_count: 2, bg_count: 1 }, "session-start.done");
199
+ log.info(
200
+ {
201
+ total_ms: Math.round(totalDur),
202
+ blocking_count: 3,
203
+ deferred_count: 2,
204
+ bg_count: 1,
205
+ },
206
+ "session-start.done",
207
+ );
166
208
 
167
209
  return {
168
210
  stdout: blocking.stdout,
@@ -17,18 +17,28 @@ const FALLBACK_COOLDOWN_MS = { codex: 5 * 3600_000, gemini: 86400_000 };
17
17
  */
18
18
  export function parseRetryAfterMs(text, provider) {
19
19
  // 1. ISO timestamp: "try again at 2026-04-11T10:00:00"
20
- const isoMatch = text.match(/(?:try again|retry|available|resets?)\s+(?:at|after)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i);
20
+ const isoMatch = text.match(
21
+ /(?:try again|retry|available|resets?)\s+(?:at|after)\s+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i,
22
+ );
21
23
  if (isoMatch) {
22
24
  const target = new Date(isoMatch[1]).getTime();
23
25
  if (target > Date.now()) return target - Date.now();
24
26
  }
25
27
 
26
28
  // 2. Duration: "N seconds/minutes/hours/days"
27
- const durMatch = text.match(/(?:retry|wait|resets?|again)\s+(?:in\s+|after\s+)?(\d+)\s*(second|minute|hour|day|week)/i);
29
+ const durMatch = text.match(
30
+ /(?:retry|wait|resets?|again)\s+(?:in\s+|after\s+)?(\d+)\s*(second|minute|hour|day|week)/i,
31
+ );
28
32
  if (durMatch) {
29
33
  const n = Number(durMatch[1]);
30
34
  const unit = durMatch[2].toLowerCase();
31
- const multipliers = { second: 1000, minute: 60_000, hour: 3600_000, day: 86400_000, week: 604800_000 };
35
+ const multipliers = {
36
+ second: 1000,
37
+ minute: 60_000,
38
+ hour: 3600_000,
39
+ day: 86400_000,
40
+ week: 604800_000,
41
+ };
32
42
  return n * (multipliers[unit] || 3600_000);
33
43
  }
34
44
 
@@ -37,7 +47,12 @@ export function parseRetryAfterMs(text, provider) {
37
47
  if (standaloneMatch) {
38
48
  const n = Number(standaloneMatch[1]);
39
49
  const unit = standaloneMatch[2].toLowerCase();
40
- const multipliers = { minute: 60_000, hour: 3600_000, day: 86400_000, week: 604800_000 };
50
+ const multipliers = {
51
+ minute: 60_000,
52
+ hour: 3600_000,
53
+ day: 86400_000,
54
+ week: 604800_000,
55
+ };
41
56
  const parsed = n * (multipliers[unit] || 3600_000);
42
57
  if (parsed >= 3600_000) return parsed; // 1시간 이상만 신뢰
43
58
  }
@@ -286,7 +301,12 @@ export async function executeWithCircuitBroker({
286
301
  const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
287
302
  const coolMs = parseRetryAfterMs(text, provider);
288
303
  brokerMod.broker.markRateLimited(lease.id, coolMs);
289
- brokerMod.broker.emit("cooldown", { id: lease.id, provider, coolMs, reason: "quota_exhausted" });
304
+ brokerMod.broker.emit("cooldown", {
305
+ id: lease.id,
306
+ provider,
307
+ coolMs,
308
+ reason: "quota_exhausted",
309
+ });
290
310
  } else {
291
311
  brokerMod.broker.release(lease.id, { ok: false });
292
312
  }
@@ -21,18 +21,36 @@ export function detectShell() {
21
21
 
22
22
  if (platform === "win32") {
23
23
  try {
24
- const path = execFileSync("where", ["pwsh.exe"], PIPE_OPTS).trim().split(/\r?\n/)[0];
24
+ const path = execFileSync("where", ["pwsh.exe"], PIPE_OPTS)
25
+ .trim()
26
+ .split(/\r?\n/)[0];
25
27
  let version = null;
26
28
  try {
27
- version = execFileSync(path, ["-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"], PIPE_OPTS).trim();
29
+ version = execFileSync(
30
+ path,
31
+ [
32
+ "-NoLogo",
33
+ "-NoProfile",
34
+ "-Command",
35
+ "$PSVersionTable.PSVersion.ToString()",
36
+ ],
37
+ PIPE_OPTS,
38
+ ).trim();
28
39
  } catch {}
29
40
  _shellCache = { name: "pwsh", path, version };
30
41
  } catch {
31
42
  try {
32
- const path = execFileSync("where", ["powershell.exe"], PIPE_OPTS).trim().split(/\r?\n/)[0];
43
+ const path = execFileSync("where", ["powershell.exe"], PIPE_OPTS)
44
+ .trim()
45
+ .split(/\r?\n/)[0];
33
46
  _shellCache = { name: "powershell", path, version: null };
34
47
  } catch {
35
- _shellCache = { name: "powershell", path: "", version: null, installHint: "pwsh: winget install Microsoft.PowerShell" };
48
+ _shellCache = {
49
+ name: "powershell",
50
+ path: "",
51
+ version: null,
52
+ installHint: "pwsh: winget install Microsoft.PowerShell",
53
+ };
36
54
  }
37
55
  }
38
56
  return _shellCache;
@@ -60,7 +78,11 @@ export function detectTerminal() {
60
78
  execFileSync("where", ["wt.exe"], PIPE_OPTS);
61
79
  _terminalCache = { name: "windows-terminal", hasWt: true };
62
80
  } catch {
63
- _terminalCache = { name: "unknown", hasWt: false, installHint: "wt: winget install Microsoft.WindowsTerminal" };
81
+ _terminalCache = {
82
+ name: "unknown",
83
+ hasWt: false,
84
+ installHint: "wt: winget install Microsoft.WindowsTerminal",
85
+ };
64
86
  }
65
87
  return _terminalCache;
66
88
  }
@@ -92,10 +114,15 @@ export function detectMultiplexer() {
92
114
  const path = execFileSync(cmd, ["tmux"], PIPE_OPTS).trim();
93
115
  _multiplexerCache = { name: "tmux", path };
94
116
  } catch {
95
- const hint = osPlatform() === "win32"
96
- ? "tmux: install tmux in WSL or MSYS2"
97
- : undefined;
98
- _multiplexerCache = { name: "none", path: null, ...(hint ? { installHint: hint } : {}) };
117
+ const hint =
118
+ osPlatform() === "win32"
119
+ ? "tmux: install tmux in WSL or MSYS2"
120
+ : undefined;
121
+ _multiplexerCache = {
122
+ name: "none",
123
+ path: null,
124
+ ...(hint ? { installHint: hint } : {}),
125
+ };
99
126
  }
100
127
  return _multiplexerCache;
101
128
  }
package/hub/pipe.mjs CHANGED
@@ -674,7 +674,10 @@ export function createPipeServer({
674
674
  const frame = safeJsonParse(line);
675
675
  await handleFrame(client, frame);
676
676
  } catch (err) {
677
- pipeLog.error({ clientId: client.id, err: String(err?.message || err) }, "pipe.frame_handler_error");
677
+ pipeLog.error(
678
+ { clientId: client.id, err: String(err?.message || err) },
679
+ "pipe.frame_handler_error",
680
+ );
678
681
  }
679
682
  }
680
683
  newlineIndex = client.buffer.indexOf("\n");
package/hub/platform.mjs CHANGED
@@ -178,7 +178,9 @@ export function killProcess(pid, options = {}) {
178
178
  if (tree) {
179
179
  try {
180
180
  execSync(`pkill -P ${numericPid}`, { stdio: "ignore", timeout: 3000 });
181
- } catch { /* 자식 없으면 무시 */ }
181
+ } catch {
182
+ /* 자식 없으면 무시 */
183
+ }
182
184
  }
183
185
  process.kill(numericPid, signal);
184
186
  return true;