triflux 10.3.4 → 10.7.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 (329) hide show
  1. package/LICENSE +21 -21
  2. package/bin/tfx-doctor-tui.mjs +1 -1
  3. package/bin/tfx-doctor.mjs +6 -1
  4. package/bin/tfx-profile.mjs +1 -1
  5. package/bin/tfx-setup-tui.mjs +1 -1
  6. package/bin/tfx-setup.mjs +6 -1
  7. package/bin/triflux.mjs +2396 -1140
  8. package/hooks/agent-route-guard.mjs +12 -8
  9. package/hooks/cross-review-tracker.mjs +21 -8
  10. package/hooks/error-context.mjs +19 -7
  11. package/hooks/hook-adaptive-collector.mjs +18 -16
  12. package/hooks/hook-manager.mjs +93 -32
  13. package/hooks/hook-orchestrator.mjs +108 -24
  14. package/hooks/hook-registry.json +11 -0
  15. package/hooks/keyword-rules.json +6 -10
  16. package/hooks/lib/resolve-root.mjs +1 -1
  17. package/hooks/mcp-config-watcher.mjs +6 -2
  18. package/hooks/pipeline-stop.mjs +3 -6
  19. package/hooks/safety-guard.mjs +99 -28
  20. package/hooks/session-start-fast.mjs +143 -0
  21. package/hooks/subagent-verifier.mjs +5 -4
  22. package/hub/account-broker.mjs +256 -60
  23. package/hub/adaptive-diagnostic.mjs +75 -48
  24. package/hub/adaptive-inject.mjs +95 -57
  25. package/hub/adaptive-memory.mjs +156 -42
  26. package/hub/adaptive.mjs +60 -31
  27. package/hub/assign-callbacks.mjs +67 -30
  28. package/hub/bridge.mjs +0 -1
  29. package/hub/cli-adapter-base.mjs +200 -48
  30. package/hub/codex-adapter.mjs +76 -96
  31. package/hub/codex-compat.mjs +3 -3
  32. package/hub/codex-preflight.mjs +63 -37
  33. package/hub/delegator/contracts.mjs +19 -23
  34. package/hub/delegator/index.mjs +3 -3
  35. package/hub/delegator/service.mjs +88 -64
  36. package/hub/delegator/tool-definitions.mjs +5 -5
  37. package/hub/fullcycle.mjs +33 -17
  38. package/hub/gemini-adapter.mjs +69 -94
  39. package/hub/hitl.mjs +89 -30
  40. package/hub/intent.mjs +161 -38
  41. package/hub/lib/cache-guard.mjs +43 -17
  42. package/hub/lib/mcp-response-cache.mjs +66 -32
  43. package/hub/lib/memory-store.mjs +285 -111
  44. package/hub/lib/path-utils.mjs +35 -37
  45. package/hub/lib/process-utils.mjs +106 -37
  46. package/hub/lib/spawn-trace.mjs +527 -0
  47. package/hub/lib/ssh-command.mjs +34 -4
  48. package/hub/lib/ssh-retry.mjs +5 -1
  49. package/hub/lib/uuidv7.mjs +4 -3
  50. package/hub/memory-doctor.mjs +266 -106
  51. package/hub/middleware/request-logger.mjs +61 -34
  52. package/hub/paths.mjs +9 -9
  53. package/hub/pipeline/gates/confidence.mjs +34 -15
  54. package/hub/pipeline/gates/consensus.mjs +27 -15
  55. package/hub/pipeline/gates/index.mjs +7 -3
  56. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  57. package/hub/pipeline/index.mjs +77 -42
  58. package/hub/pipeline/state.mjs +10 -10
  59. package/hub/pipeline/transitions.mjs +40 -23
  60. package/hub/platform.mjs +57 -48
  61. package/hub/promote-penalties.mjs +25 -7
  62. package/hub/quality/deslop.mjs +70 -49
  63. package/hub/research.mjs +32 -25
  64. package/hub/router.mjs +240 -107
  65. package/hub/routing/complexity.mjs +132 -29
  66. package/hub/routing/index.mjs +17 -12
  67. package/hub/routing/q-learning.mjs +76 -28
  68. package/hub/server.mjs +4 -4
  69. package/hub/session-fingerprint.mjs +126 -60
  70. package/hub/state.mjs +84 -43
  71. package/hub/store-adapter.mjs +59 -26
  72. package/hub/store.mjs +356 -153
  73. package/hub/team/agent-map.json +22 -7
  74. package/hub/team/ansi.mjs +186 -122
  75. package/hub/team/backend.mjs +28 -10
  76. package/hub/team/cli/commands/attach.mjs +29 -9
  77. package/hub/team/cli/commands/control.mjs +29 -8
  78. package/hub/team/cli/commands/debug.mjs +32 -11
  79. package/hub/team/cli/commands/focus.mjs +38 -11
  80. package/hub/team/cli/commands/interrupt.mjs +18 -6
  81. package/hub/team/cli/commands/kill.mjs +16 -5
  82. package/hub/team/cli/commands/list.mjs +11 -4
  83. package/hub/team/cli/commands/send.mjs +19 -6
  84. package/hub/team/cli/commands/start/index.mjs +154 -31
  85. package/hub/team/cli/commands/start/parse-args.mjs +38 -11
  86. package/hub/team/cli/commands/start/start-headless.mjs +112 -36
  87. package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
  88. package/hub/team/cli/commands/start/start-mux.mjs +70 -21
  89. package/hub/team/cli/commands/start/start-wt.mjs +29 -12
  90. package/hub/team/cli/commands/status.mjs +43 -14
  91. package/hub/team/cli/commands/stop.mjs +11 -4
  92. package/hub/team/cli/commands/task.mjs +8 -3
  93. package/hub/team/cli/commands/tasks.mjs +1 -1
  94. package/hub/team/cli/index.mjs +2 -2
  95. package/hub/team/cli/manifest.mjs +38 -8
  96. package/hub/team/cli/render.mjs +30 -8
  97. package/hub/team/cli/services/attach-fallback.mjs +31 -11
  98. package/hub/team/cli/services/hub-client.mjs +42 -14
  99. package/hub/team/cli/services/member-selector.mjs +11 -4
  100. package/hub/team/cli/services/native-control.mjs +48 -21
  101. package/hub/team/cli/services/runtime-mode.mjs +2 -1
  102. package/hub/team/cli/services/state-store.mjs +25 -8
  103. package/hub/team/cli/services/task-model.mjs +16 -6
  104. package/hub/team/conductor-mesh-bridge.mjs +24 -23
  105. package/hub/team/conductor.mjs +8 -4
  106. package/hub/team/dashboard-anchor.mjs +4 -5
  107. package/hub/team/dashboard-layout.mjs +3 -1
  108. package/hub/team/dashboard-open.mjs +41 -21
  109. package/hub/team/dashboard.mjs +76 -28
  110. package/hub/team/event-log.mjs +18 -10
  111. package/hub/team/handoff.mjs +31 -15
  112. package/hub/team/headless.mjs +2 -1
  113. package/hub/team/health-probe.mjs +69 -54
  114. package/hub/team/launcher-template.mjs +16 -13
  115. package/hub/team/native-supervisor.mjs +65 -21
  116. package/hub/team/native.mjs +74 -35
  117. package/hub/team/nativeProxy.mjs +184 -113
  118. package/hub/team/notify.mjs +119 -76
  119. package/hub/team/orchestrator.mjs +9 -4
  120. package/hub/team/pane.mjs +12 -7
  121. package/hub/team/process-cleanup.mjs +25 -16
  122. package/hub/team/psmux.mjs +491 -201
  123. package/hub/team/remote-probe.mjs +68 -52
  124. package/hub/team/remote-session.mjs +117 -59
  125. package/hub/team/remote-watcher.mjs +61 -33
  126. package/hub/team/routing.mjs +51 -25
  127. package/hub/team/runtime-strategy.mjs +3 -1
  128. package/hub/team/session.mjs +98 -34
  129. package/hub/team/staleState.mjs +72 -30
  130. package/hub/team/swarm-locks.mjs +15 -13
  131. package/hub/team/swarm-planner.mjs +32 -21
  132. package/hub/team/swarm-reconciler.mjs +48 -23
  133. package/hub/team/tui-lite.mjs +266 -68
  134. package/hub/team/tui-remote-adapter.mjs +14 -10
  135. package/hub/team/tui-viewer.mjs +99 -43
  136. package/hub/team/tui.mjs +708 -271
  137. package/hub/team/worktree-lifecycle.mjs +152 -58
  138. package/hub/team/wt-manager.mjs +24 -14
  139. package/hub/token-mode.mjs +71 -71
  140. package/hub/tray.mjs +66 -23
  141. package/hub/workers/claude-worker.mjs +162 -118
  142. package/hub/workers/codex-mcp.mjs +192 -141
  143. package/hub/workers/delegator-mcp.mjs +507 -333
  144. package/hub/workers/factory.mjs +8 -8
  145. package/hub/workers/gemini-worker.mjs +115 -84
  146. package/hub/workers/interface.mjs +6 -1
  147. package/hub/workers/worker-utils.mjs +21 -14
  148. package/hud/colors.mjs +27 -9
  149. package/hud/constants.mjs +162 -26
  150. package/hud/context-monitor.mjs +82 -41
  151. package/hud/hud-qos-status.mjs +129 -49
  152. package/hud/mission-board.mjs +6 -3
  153. package/hud/providers/claude.mjs +226 -115
  154. package/hud/providers/codex.mjs +62 -22
  155. package/hud/providers/gemini.mjs +168 -56
  156. package/hud/renderers.mjs +384 -119
  157. package/hud/terminal.mjs +101 -31
  158. package/hud/utils.mjs +78 -38
  159. package/mesh/index.mjs +11 -5
  160. package/mesh/mesh-budget.mjs +18 -9
  161. package/mesh/mesh-heartbeat.mjs +1 -1
  162. package/mesh/mesh-queue.mjs +3 -5
  163. package/mesh/mesh-router.mjs +5 -4
  164. package/package.json +2 -1
  165. package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
  166. package/scripts/__tests__/keyword-detector.test.mjs +77 -28
  167. package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
  168. package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
  169. package/scripts/__tests__/remote-spawn.test.mjs +10 -4
  170. package/scripts/__tests__/session-start-fast.test.mjs +36 -0
  171. package/scripts/__tests__/skill-template.test.mjs +98 -50
  172. package/scripts/__tests__/smoke.test.mjs +1 -1
  173. package/scripts/__tests__/spawn-trace.test.mjs +102 -0
  174. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
  175. package/scripts/cache-doctor.mjs +11 -4
  176. package/scripts/cache-warmup.mjs +96 -37
  177. package/scripts/claudemd-sync.mjs +27 -17
  178. package/scripts/codex-gateway-preflight.mjs +52 -37
  179. package/scripts/codex-mcp-gateway-sync.mjs +59 -39
  180. package/scripts/completions/tfx.bash +47 -47
  181. package/scripts/completions/tfx.fish +44 -44
  182. package/scripts/completions/tfx.zsh +83 -83
  183. package/scripts/config-audit.mjs +232 -0
  184. package/scripts/convert-to-tmpl.mjs +54 -0
  185. package/scripts/cross-review-gate.mjs +35 -12
  186. package/scripts/cross-review-tracker.mjs +21 -8
  187. package/scripts/demo.mjs +35 -17
  188. package/scripts/doctor-diagnose.mjs +284 -0
  189. package/scripts/gen-skill-docs.mjs +7 -2
  190. package/scripts/gen-skill-manifest.mjs +2 -1
  191. package/scripts/headless-guard.mjs +86 -48
  192. package/scripts/hub-ensure.mjs +45 -26
  193. package/scripts/keyword-detector.mjs +41 -20
  194. package/scripts/keyword-rules-expander.mjs +47 -30
  195. package/scripts/lib/claudemd-scanner.mjs +6 -1
  196. package/scripts/lib/context.mjs +3 -3
  197. package/scripts/lib/cross-review-utils.mjs +6 -3
  198. package/scripts/lib/env-probe.mjs +47 -28
  199. package/scripts/lib/gemini-profiles.mjs +44 -10
  200. package/scripts/lib/handoff.mjs +33 -17
  201. package/scripts/lib/hook-utils.mjs +8 -6
  202. package/scripts/lib/keyword-rules.mjs +43 -19
  203. package/scripts/lib/logger.mjs +24 -24
  204. package/scripts/lib/mcp-filter.mjs +377 -239
  205. package/scripts/lib/mcp-guard-engine.mjs +194 -79
  206. package/scripts/lib/mcp-manifest.mjs +23 -13
  207. package/scripts/lib/mcp-server-catalog.mjs +300 -63
  208. package/scripts/lib/psmux-info.mjs +11 -6
  209. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  210. package/scripts/lib/skill-template.mjs +30 -7
  211. package/scripts/mcp-check.mjs +58 -39
  212. package/scripts/mcp-gateway-config.mjs +83 -39
  213. package/scripts/mcp-gateway-ensure.mjs +43 -35
  214. package/scripts/mcp-gateway-integration-test.mjs +70 -58
  215. package/scripts/mcp-gateway-start.mjs +126 -60
  216. package/scripts/mcp-gateway-verify.mjs +24 -22
  217. package/scripts/mcp-safety-guard.mjs +44 -11
  218. package/scripts/notion-read.mjs +199 -84
  219. package/scripts/pack.mjs +94 -89
  220. package/scripts/preflight-cache.mjs +27 -10
  221. package/scripts/preinstall.mjs +42 -13
  222. package/scripts/remote-spawn.mjs +309 -94
  223. package/scripts/run.cjs +8 -5
  224. package/scripts/session-spawn-helper.mjs +130 -39
  225. package/scripts/session-stale-cleanup.mjs +123 -0
  226. package/scripts/setup.mjs +941 -492
  227. package/scripts/test-lock.mjs +20 -7
  228. package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
  229. package/scripts/tfx-batch-stats.mjs +32 -11
  230. package/scripts/tfx-gate-activate.mjs +11 -4
  231. package/scripts/tfx-route-post.mjs +87 -20
  232. package/scripts/tfx-route-worker.mjs +57 -51
  233. package/scripts/tfx-route.sh +41 -124
  234. package/scripts/tmp-cleanup.mjs +21 -7
  235. package/scripts/token-snapshot.mjs +204 -85
  236. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  237. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  238. package/skills/.omc/state/last-tool-error.json +7 -0
  239. package/skills/.omc/state/subagent-tracking.json +7 -0
  240. package/skills/_templates/base.md +1 -6
  241. package/skills/merge-worktree/SKILL.md.tmpl +144 -0
  242. package/skills/shared/telemetry-segment.md +6 -0
  243. package/skills/star-prompt/SKILL.md.tmpl +222 -0
  244. package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
  245. package/skills/tfx-analysis/skill.json +1 -6
  246. package/skills/tfx-auto/SKILL.md +1 -0
  247. package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
  248. package/skills/tfx-auto-codex/skill.json +1 -3
  249. package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
  250. package/skills/tfx-autopilot/skill.json +1 -5
  251. package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
  252. package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
  253. package/skills/tfx-autoroute/skill.json +1 -7
  254. package/skills/tfx-codex/SKILL.md +1 -0
  255. package/skills/tfx-codex/skill.json +1 -3
  256. package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
  257. package/skills/tfx-codex-swarm/evals/evals.json +1 -1
  258. package/skills/tfx-codex-swarm/skill.json +1 -4
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
  265. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
  266. package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
  267. package/skills/tfx-debate/SKILL.md.tmpl +192 -0
  268. package/skills/tfx-debate/skill.json +1 -7
  269. package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
  270. package/skills/tfx-deep-analysis/skill.json +1 -5
  271. package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
  272. package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
  273. package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
  274. package/skills/tfx-deep-qa/skill.json +1 -6
  275. package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
  276. package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
  277. package/skills/tfx-doctor/SKILL.md +21 -0
  278. package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
  279. package/skills/tfx-doctor/skill.json +1 -3
  280. package/skills/tfx-find/SKILL.md +1 -0
  281. package/skills/tfx-forge/SKILL.md.tmpl +187 -0
  282. package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
  283. package/skills/tfx-fullcycle/skill.json +1 -6
  284. package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
  285. package/skills/tfx-gemini/skill.json +1 -3
  286. package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
  287. package/skills/tfx-hooks/skill.json +1 -3
  288. package/skills/tfx-hub/SKILL.md.tmpl +212 -0
  289. package/skills/tfx-hub/skill.json +1 -3
  290. package/skills/tfx-index/SKILL.md +1 -0
  291. package/skills/tfx-index/skill.json +1 -6
  292. package/skills/tfx-interview/SKILL.md.tmpl +285 -0
  293. package/skills/tfx-multi/SKILL.md.tmpl +183 -0
  294. package/skills/tfx-multi/skill.json +1 -3
  295. package/skills/tfx-panel/SKILL.md.tmpl +189 -0
  296. package/skills/tfx-panel/skill.json +1 -7
  297. package/skills/tfx-persist/SKILL.md.tmpl +270 -0
  298. package/skills/tfx-persist/skill.json +1 -7
  299. package/skills/tfx-plan/SKILL.md +1 -0
  300. package/skills/tfx-plan/skill.json +1 -6
  301. package/skills/tfx-profile/SKILL.md.tmpl +239 -0
  302. package/skills/tfx-profile/skill.json +1 -3
  303. package/skills/tfx-prune/SKILL.md.tmpl +200 -0
  304. package/skills/tfx-prune/skill.json +1 -7
  305. package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
  306. package/skills/tfx-psmux-rules/skill.json +1 -4
  307. package/skills/tfx-qa/SKILL.md +1 -0
  308. package/skills/tfx-qa/skill.json +1 -6
  309. package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
  310. package/skills/tfx-ralph/skill.json +1 -4
  311. package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
  312. package/skills/tfx-remote-setup/skill.json +1 -3
  313. package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
  314. package/skills/tfx-remote-spawn/references/hosts.json +16 -0
  315. package/skills/tfx-remote-spawn/skill.json +1 -4
  316. package/skills/tfx-research/SKILL.md +1 -0
  317. package/skills/tfx-review/SKILL.md +1 -0
  318. package/skills/tfx-review/skill.json +1 -6
  319. package/skills/tfx-setup/SKILL.md.tmpl +504 -0
  320. package/skills/tfx-setup/skill.json +1 -3
  321. package/skills/tfx-swarm/SKILL.md +22 -0
  322. package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
  323. package/tui/codex-profile.mjs +88 -33
  324. package/tui/core.mjs +45 -15
  325. package/tui/doctor.mjs +75 -28
  326. package/tui/gemini-profile.mjs +74 -29
  327. package/tui/monitor-data.mjs +8 -4
  328. package/tui/monitor.mjs +71 -27
  329. package/tui/setup.mjs +133 -42
package/scripts/setup.mjs CHANGED
@@ -1,15 +1,29 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  // triflux 세션 시작 시 자동 설정 스크립트
3
4
  // - tfx-route.sh를 ~/.claude/scripts/에 동기화
4
5
  // - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
5
6
  // - skills/를 ~/.claude/skills/에 동기화
6
7
 
7
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
8
- import { join, dirname, relative } from "path";
8
+ import { execFileSync, spawn } from "child_process";
9
+ import {
10
+ chmodSync,
11
+ copyFileSync,
12
+ existsSync,
13
+ mkdirSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ unlinkSync,
17
+ writeFileSync,
18
+ } from "fs";
9
19
  import { homedir } from "os";
10
- import { spawn, execFileSync } from "child_process";
20
+ import { dirname, join, relative } from "path";
11
21
  import { fileURLToPath } from "url";
12
- import { ensureGlobalClaudeRoutingSection, ensureTfxSection, getLatestRoutingTable } from "./claudemd-sync.mjs";
22
+ import {
23
+ ensureGlobalClaudeRoutingSection,
24
+ ensureTfxSection,
25
+ getLatestRoutingTable,
26
+ } from "./claudemd-sync.mjs";
13
27
  import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
14
28
 
15
29
  const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -30,28 +44,21 @@ function detectDevMode(root = PLUGIN_ROOT) {
30
44
  }
31
45
 
32
46
  const BREADCRUMB_PATH = join(CLAUDE_DIR, "scripts", ".tfx-pkg-root");
47
+ const SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
48
+ const HUD_PATH = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
33
49
 
34
50
  const REQUIRED_CODEX_PROFILES = [
35
51
  {
36
52
  name: "codex53_high",
37
- lines: [
38
- 'model = "gpt-5.3-codex"',
39
- 'model_reasoning_effort = "high"',
40
- ],
53
+ lines: ['model = "gpt-5.3-codex"', 'model_reasoning_effort = "high"'],
41
54
  },
42
55
  {
43
56
  name: "codex53_xhigh",
44
- lines: [
45
- 'model = "gpt-5.3-codex"',
46
- 'model_reasoning_effort = "xhigh"',
47
- ],
57
+ lines: ['model = "gpt-5.3-codex"', 'model_reasoning_effort = "xhigh"'],
48
58
  },
49
59
  {
50
60
  name: "spark53_low",
51
- lines: [
52
- 'model = "gpt-5.3-codex-spark"',
53
- 'model_reasoning_effort = "low"',
54
- ],
61
+ lines: ['model = "gpt-5.3-codex-spark"', 'model_reasoning_effort = "low"'],
55
62
  },
56
63
  ];
57
64
 
@@ -62,8 +69,9 @@ function scanHudFiles(pluginRoot, claudeDir) {
62
69
  if (!existsSync(hudRoot)) return [];
63
70
 
64
71
  const walk = (currentDir) => {
65
- const entries = readdirSync(currentDir, { withFileTypes: true })
66
- .sort((left, right) => left.name.localeCompare(right.name));
72
+ const entries = readdirSync(currentDir, { withFileTypes: true }).sort(
73
+ (left, right) => left.name.localeCompare(right.name),
74
+ );
67
75
 
68
76
  return entries.flatMap((entry) => {
69
77
  const absolutePath = join(currentDir, entry.name);
@@ -71,20 +79,27 @@ function scanHudFiles(pluginRoot, claudeDir) {
71
79
  return walk(absolutePath);
72
80
  }
73
81
 
74
- if (!entry.isFile() || HUD_SYNC_EXCLUDES.has(entry.name) || !entry.name.endsWith(".mjs")) {
82
+ if (
83
+ !entry.isFile() ||
84
+ HUD_SYNC_EXCLUDES.has(entry.name) ||
85
+ !entry.name.endsWith(".mjs")
86
+ ) {
75
87
  return [];
76
88
  }
77
89
 
78
90
  const hudRelativePath = relative(hudRoot, absolutePath);
79
91
  const normalizedRelativePath = hudRelativePath.replace(/\\/g, "/");
80
92
 
81
- return [{
82
- src: absolutePath,
83
- dst: join(claudeDir, "hud", hudRelativePath),
84
- label: normalizedRelativePath === "hud-qos-status.mjs"
85
- ? "hud-qos-status.mjs"
86
- : `hud/${normalizedRelativePath}`,
87
- }];
93
+ return [
94
+ {
95
+ src: absolutePath,
96
+ dst: join(claudeDir, "hud", hudRelativePath),
97
+ label:
98
+ normalizedRelativePath === "hud-qos-status.mjs"
99
+ ? "hud-qos-status.mjs"
100
+ : `hud/${normalizedRelativePath}`,
101
+ },
102
+ ];
88
103
  });
89
104
  };
90
105
 
@@ -218,7 +233,8 @@ function shouldSyncTextFile(src, dst) {
218
233
 
219
234
  function getPackageVersion() {
220
235
  try {
221
- return JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf8")).version;
236
+ return JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf8"))
237
+ .version;
222
238
  } catch {
223
239
  return null;
224
240
  }
@@ -237,7 +253,11 @@ function readMarker() {
237
253
  function writeMarker(marker) {
238
254
  const markerDir = dirname(SETUP_MARKER_PATH);
239
255
  if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
240
- writeFileSync(SETUP_MARKER_PATH, JSON.stringify(marker, null, 2) + "\n", "utf8");
256
+ writeFileSync(
257
+ SETUP_MARKER_PATH,
258
+ JSON.stringify(marker, null, 2) + "\n",
259
+ "utf8",
260
+ );
241
261
  }
242
262
 
243
263
  function escapeRegExp(value) {
@@ -288,18 +308,11 @@ const SKILL_ALIASES = [
288
308
 
289
309
  // ── 폐기 예정 스킬 목록 ──
290
310
 
291
- const DEPRECATED_SKILLS = [
292
- "tfx-codex-route",
293
- "tfx-gemini-route",
294
- ];
311
+ const DEPRECATED_SKILLS = ["tfx-codex-route", "tfx-gemini-route"];
295
312
 
296
313
  // ── 구형 Codex 모델 (마이그레이션 안내 대상) ──
297
314
 
298
- const LEGACY_CODEX_MODELS = [
299
- "o4-mini",
300
- "o3",
301
- "codex-mini-latest",
302
- ];
315
+ const LEGACY_CODEX_MODELS = ["o4-mini", "o3", "codex-mini-latest"];
303
316
 
304
317
  /**
305
318
  * 별칭 스킬 디렉토리를 동기화한다.
@@ -362,8 +375,12 @@ function cleanupStaleSkills(installedDir, pkgDir) {
362
375
  const entries = readdirSync(skillPath);
363
376
  for (const f of entries) unlinkSync(join(skillPath, f));
364
377
  // rmdir only works on empty dirs; ignore errors for nested
365
- try { readdirSync(skillPath).length === 0 && unlinkSync(skillPath); } catch {}
366
- } catch { /* best effort */ }
378
+ try {
379
+ readdirSync(skillPath).length === 0 && unlinkSync(skillPath);
380
+ } catch {}
381
+ } catch {
382
+ /* best effort */
383
+ }
367
384
  removed.push(name);
368
385
  }
369
386
  return { count: removed.length, removed };
@@ -436,9 +453,12 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
436
453
  settings.hooks[spec.event] = [];
437
454
  }
438
455
  const entries = settings.hooks[spec.event];
439
- const alreadyRegistered = entries.some((entry) =>
440
- Array.isArray(entry?.hooks) &&
441
- entry.hooks.some((h) => extractManagedHookFilename(h?.command) === spec.fileName),
456
+ const alreadyRegistered = entries.some(
457
+ (entry) =>
458
+ Array.isArray(entry?.hooks) &&
459
+ entry.hooks.some(
460
+ (h) => extractManagedHookFilename(h?.command) === spec.fileName,
461
+ ),
442
462
  );
443
463
  if (alreadyRegistered) continue;
444
464
 
@@ -450,7 +470,11 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
450
470
  }
451
471
 
452
472
  if (added.length > 0) {
453
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
473
+ writeFileSync(
474
+ settingsPath,
475
+ JSON.stringify(settings, null, 2) + "\n",
476
+ "utf8",
477
+ );
454
478
  }
455
479
  return { ok: true, changed: added.length > 0, added };
456
480
  } catch {
@@ -463,13 +487,19 @@ function ensureHooksInSettings({ settingsPath, registryPath }) {
463
487
  * @param {{ mcpUrl: string, createIfMissing?: boolean, enabled?: boolean }} opts
464
488
  * @returns {{ ok: boolean, changed: boolean, reason?: string }}
465
489
  */
466
- function ensureCodexHubServerConfig({ configFile, mcpUrl, createIfMissing = false, enabled = false }) {
490
+ function ensureCodexHubServerConfig({
491
+ configFile,
492
+ mcpUrl,
493
+ createIfMissing = false,
494
+ enabled = false,
495
+ }) {
467
496
  try {
468
497
  const codexConfigDir = join(homedir(), ".codex");
469
498
  const configPath = configFile || join(codexConfigDir, "config.json");
470
499
 
471
500
  if (!existsSync(configPath)) {
472
- if (!createIfMissing) return { ok: true, changed: false, reason: "no-config" };
501
+ if (!createIfMissing)
502
+ return { ok: true, changed: false, reason: "no-config" };
473
503
  const dir = dirname(configPath);
474
504
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
475
505
  const config = { mcpServers: { "tfx-hub": { url: mcpUrl, enabled } } };
@@ -483,7 +513,11 @@ function ensureCodexHubServerConfig({ configFile, mcpUrl, createIfMissing = fals
483
513
  const existing = config.mcpServers["tfx-hub"];
484
514
  const desired = { ...(existing || {}), url: mcpUrl, enabled };
485
515
 
486
- if (existing && existing.url === desired.url && existing.enabled === desired.enabled) {
516
+ if (
517
+ existing &&
518
+ existing.url === desired.url &&
519
+ existing.enabled === desired.enabled
520
+ ) {
487
521
  return { ok: true, changed: false };
488
522
  }
489
523
 
@@ -528,9 +562,9 @@ function ensureCodexProfiles() {
528
562
 
529
563
  // headless 모드에서 승인 없이 실행하려면 sandbox 설정 필수
530
564
  // Codex 0.117.0+: config.toml 설정과 CLI 플래그 중복 시 에러
531
- if (process.platform === "win32" && !updated.includes('[windows]')) {
565
+ if (process.platform === "win32" && !updated.includes("[windows]")) {
532
566
  if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
533
- updated += "\n[windows]\nsandbox = \"elevated\"\n";
567
+ updated += '\n[windows]\nsandbox = "elevated"\n';
534
568
  changed++;
535
569
  }
536
570
 
@@ -540,7 +574,10 @@ function ensureCodexProfiles() {
540
574
 
541
575
  return { ok: true, changed };
542
576
  } catch (error) {
543
- const message = error instanceof Error && error.message ? error.message.trim() : "unknown error";
577
+ const message =
578
+ error instanceof Error && error.message
579
+ ? error.message.trim()
580
+ : "unknown error";
544
581
  return { ok: false, changed: 0, message };
545
582
  }
546
583
  }
@@ -553,301 +590,105 @@ function syncClaudeRoutingSections() {
553
590
  ensureGlobalClaudeRoutingSection(CLAUDE_DIR),
554
591
  ];
555
592
  } catch (error) {
556
- const reason = error instanceof Error ? error.message : "routing_sync_failed";
557
- return [{ action: "unchanged", path: join(PLUGIN_ROOT, "CLAUDE.md"), skipped: true, reason }];
558
- }
559
- }
560
-
561
- export {
562
- replaceProfileSection,
563
- hasProfileSection,
564
- detectDevMode,
565
- scanHudFiles,
566
- SYNC_MAP,
567
- BREADCRUMB_PATH,
568
- PLUGIN_ROOT,
569
- CLAUDE_DIR,
570
- SETUP_MARKER_PATH,
571
- readMarker,
572
- writeMarker,
573
- REQUIRED_CODEX_PROFILES,
574
- getVersion,
575
- ensureCodexProfiles,
576
- SKILL_ALIASES,
577
- LEGACY_CODEX_MODELS,
578
- DEPRECATED_SKILLS,
579
- syncAliasedSkillDir,
580
- cleanupStaleSkills,
581
- extractManagedHookFilename,
582
- getManagedRegistryHooks,
583
- ensureHooksInSettings,
584
- ensureCodexHubServerConfig,
585
- };
586
-
587
- async function main() {
588
- const isSync = process.argv.includes("--sync");
589
- const isForce = process.argv.includes("--force");
590
- const isDev = detectDevMode();
591
-
592
- if (isDev) {
593
- console.log(" [dev] \uB85C\uCEEC \uAC1C\uBC1C \uBAA8\uB4DC \uAC10\uC9C0");
594
- }
595
-
596
- if (isSync) {
597
- console.log(" [sync] \uBA85\uC2DC\uC801 \uC7AC\uB3D9\uAE30\uD654 \uC2E4\uD589");
598
- }
599
-
600
- const pkgVersion = getPackageVersion();
601
- const marker = readMarker();
602
- const claudeRoutingResults = syncClaudeRoutingSections();
603
- const claudeRoutingChangedCount = claudeRoutingResults.filter((result) => result.action === "created" || result.action === "updated").length;
604
- if (pkgVersion && marker?.version === pkgVersion && !isForce) {
605
- if (claudeRoutingChangedCount > 0) {
606
- console.log(`setup: skip core sync (v${pkgVersion} already synced, CLAUDE.md ${claudeRoutingChangedCount}건 반영)`);
607
- } else {
608
- console.log(`setup: skip (v${pkgVersion} already synced)`);
609
- }
610
- process.exit(0);
611
- }
612
-
613
- let synced = claudeRoutingChangedCount;
614
-
615
- // ── Memory Doctor (P0 자동 수정) ──
616
- const isCIEnv = process.env.CI === "true" || process.env.DOCKER === "true";
617
- if (!isCIEnv) {
618
- try {
619
- const { createMemoryDoctor } = await import("../hub/memory-doctor.mjs");
620
- const projectSlug = process.cwd().replace(/^([A-Z]):/u, "$1-").replace(/[\\/]/gu, "-");
621
- const memDir = join(CLAUDE_DIR, "projects", projectSlug, "memory");
622
- if (existsSync(memDir)) {
623
- const doctor = createMemoryDoctor({
624
- memoryDir: memDir,
625
- rulesDir: join(process.cwd(), ".claude", "rules"),
626
- projectDir: process.cwd(),
627
- claudeDir: CLAUDE_DIR,
628
- });
629
- const { checks, healthScore } = doctor.scan();
630
- const p0Auto = checks.filter((c) => c.severity === "P0" && c.autofix && !c.passed);
631
- if (p0Auto.length > 0) {
632
- doctor.fixAll({ severity: "P0" });
633
- console.log(` memory-doctor: ${p0Auto.length}건 P0 자동 수정 (health: ${healthScore})`);
634
- synced += p0Auto.length;
635
- }
636
- }
637
- } catch (err) {
638
- console.log(` memory-doctor: skip (${err.message})`);
593
+ const reason =
594
+ error instanceof Error ? error.message : "routing_sync_failed";
595
+ return [
596
+ {
597
+ action: "unchanged",
598
+ path: join(PLUGIN_ROOT, "CLAUDE.md"),
599
+ skipped: true,
600
+ reason,
601
+ },
602
+ ];
639
603
  }
640
604
  }
641
605
 
642
- for (const { src, dst } of SYNC_MAP) {
643
- if (!existsSync(src)) continue;
644
-
645
- const dstDir = dirname(dst);
646
- if (!existsSync(dstDir)) {
647
- mkdirSync(dstDir, { recursive: true });
648
- }
649
-
650
- if (!existsSync(dst)) {
651
- copyFileSync(src, dst);
652
- try { chmodSync(dst, 0o755); } catch {}
653
- synced++;
654
- } else {
655
- if (shouldSyncTextFile(src, dst)) {
656
- copyFileSync(src, dst);
657
- try { chmodSync(dst, 0o755); } catch {}
658
- synced++;
659
- }
660
- }
606
+ function createCommandIo() {
607
+ const stdout = [];
608
+ const stderr = [];
609
+
610
+ return {
611
+ log(message = "") {
612
+ stdout.push(`${message}\n`);
613
+ },
614
+ writeStdout(message = "") {
615
+ stdout.push(message);
616
+ },
617
+ writeStderr(message = "") {
618
+ stderr.push(message);
619
+ },
620
+ result(code = 0) {
621
+ return { code, stdout: stdout.join(""), stderr: stderr.join("") };
622
+ },
623
+ };
661
624
  }
662
625
 
663
- try {
664
- const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
665
- if (claudeGuide.changed) synced++;
666
- } catch (e) {
667
- console.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 라우팅: ${e.message}`);
626
+ function getSetupArgv(stdinData) {
627
+ return Array.isArray(stdinData?.argv) ? stdinData.argv : [];
668
628
  }
669
629
 
670
- // ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
671
-
672
- const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
673
- const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
674
- const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
675
-
676
- // native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
677
- const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
630
+ function loadSettings() {
631
+ if (!existsSync(SETTINGS_PATH)) return {};
678
632
 
679
- if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
680
633
  try {
681
- const { cpSync } = await import("fs");
682
- for (const entry of readdirSync(srcNodeModules)) {
683
- if (SKIP_PACKAGES.has(entry)) continue;
684
-
685
- const src = join(srcNodeModules, entry);
686
- const dst = join(workerNodeModules, entry);
687
- if (existsSync(dst)) continue;
688
-
689
- mkdirSync(dirname(dst), { recursive: true });
690
- cpSync(src, dst, { recursive: true });
691
- }
692
- synced++;
634
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf8"));
693
635
  } catch {
694
- // best effort: 의존성 복사 실패 시 exec fallback으로 동작
695
- }
696
- }
697
-
698
- // ── 패키지 루트 breadcrumb 기록 ──
699
- // tfx-route.sh가 hub/server.mjs, hub/bridge.mjs를 찾을 수 있도록
700
- // 패키지 루트 경로를 ~/.claude/scripts/.tfx-pkg-root에 기록한다.
701
- // dev mode에서는 항상 최신 경로를 기록 (--sync 시 강제 갱신).
702
- {
703
- const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
704
- const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
705
- ? readFileSync(BREADCRUMB_PATH, "utf8").trim()
706
- : "";
707
- if (currentBreadcrumb !== pkgRootForward || isSync) {
708
- const breadcrumbDir = dirname(BREADCRUMB_PATH);
709
- if (!existsSync(breadcrumbDir)) mkdirSync(breadcrumbDir, { recursive: true });
710
- writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
711
- synced++;
712
- }
713
- }
714
-
715
- // ── 에이전트 동기화 (.claude/agents/ → ~/.claude/agents/) ──
716
- // slim-wrapper 등 커스텀 에이전트를 글로벌에 배포하여
717
- // 다른 프로젝트에서도 subagent_type으로 참조 가능하게 한다.
718
-
719
- const agentsSrc = join(PLUGIN_ROOT, ".claude", "agents");
720
- const agentsDst = join(CLAUDE_DIR, "agents");
721
-
722
- if (existsSync(agentsSrc)) {
723
- if (!existsSync(agentsDst)) mkdirSync(agentsDst, { recursive: true });
724
-
725
- for (const name of readdirSync(agentsSrc)) {
726
- if (!name.endsWith(".md")) continue;
727
-
728
- const src = join(agentsSrc, name);
729
- const dst = join(agentsDst, name);
730
-
731
- if (!existsSync(dst)) {
732
- copyFileSync(src, dst);
733
- synced++;
734
- } else if (shouldSyncTextFile(src, dst)) {
735
- copyFileSync(src, dst);
736
- synced++;
737
- }
636
+ return {};
738
637
  }
739
638
  }
740
639
 
741
- // ── 스킬 동기화 ──
742
- // SKILL.md + 하위 디렉토리(references/ 등)를 재귀적으로 동기화
743
-
744
- const skillsSrc = join(PLUGIN_ROOT, "skills");
745
- const skillsDst = join(CLAUDE_DIR, "skills");
746
-
747
- function syncSkillDir(srcDir, dstDir) {
748
- if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
749
-
750
- let count = 0;
751
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
752
- const srcPath = join(srcDir, entry.name);
753
- const dstPath = join(dstDir, entry.name);
754
-
755
- if (entry.isDirectory()) {
756
- count += syncSkillDir(srcPath, dstPath);
757
- } else if (entry.name.endsWith(".md")) {
758
- if (shouldSyncTextFile(srcPath, dstPath)) {
759
- copyFileSync(srcPath, dstPath);
760
- count++;
761
- }
762
- }
763
- }
764
- return count;
765
- }
766
-
767
- if (existsSync(skillsSrc)) {
768
- for (const name of readdirSync(skillsSrc)) {
769
- const skillDir = join(skillsSrc, name);
770
- const skillMd = join(skillDir, "SKILL.md");
771
- if (!existsSync(skillMd)) continue;
772
-
773
- synced += syncSkillDir(skillDir, join(skillsDst, name));
774
- }
640
+ function persistSettings(settings) {
641
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");
775
642
  }
776
643
 
777
- // ── settings.json 통합 R/W ──
778
- // 3개 섹션(statusLine, agentTeams, hooks) 1회 read → 일괄 수정 → 1회 write
779
-
780
- const settingsPath = join(CLAUDE_DIR, "settings.json");
781
- const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
782
-
783
- /**
784
- * statusLine 섹션 적용.
785
- * @param {object} s - settings 객체 (직접 변경)
786
- * @returns {boolean} 변경 여부
787
- */
788
- function applyStatusLine(s) {
789
- if (!existsSync(hudPath)) return false;
790
- const currentCmd = s.statusLine?.command || "";
644
+ function applyStatusLine(settings) {
645
+ if (!existsSync(HUD_PATH)) return false;
646
+ const currentCmd = settings.statusLine?.command || "";
791
647
  if (currentCmd.includes("hud-qos-status.mjs")) return false;
792
648
 
793
649
  const nodePath = process.execPath.replace(/\\/g, "/");
794
- const hudForward = hudPath.replace(/\\/g, "/");
650
+ const hudForward = HUD_PATH.replace(/\\/g, "/");
795
651
  const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
796
652
  const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
797
653
 
798
- s.statusLine = { type: "command", command: `${nodeRef} ${hudRef}` };
654
+ settings.statusLine = { type: "command", command: `${nodeRef} ${hudRef}` };
799
655
  return true;
800
656
  }
801
657
 
802
- /**
803
- * Agent Teams 환경변수 섹션 적용.
804
- * @param {object} s - settings 객체 (직접 변경)
805
- * @returns {boolean} 변경 여부
806
- */
807
- function applyAgentTeams(s) {
808
- if (!s.env) s.env = {};
658
+ function applyAgentTeams(settings) {
659
+ if (!settings.env) settings.env = {};
809
660
  let changed = false;
810
661
 
811
- if (s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
812
- s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
662
+ if (settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
663
+ settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
813
664
  changed = true;
814
665
  }
815
- // teammateMode: auto (tmux 밖이면 in-process, 안이면 split-pane)
816
- if (!s.teammateMode) {
817
- s.teammateMode = "auto";
666
+ if (!settings.teammateMode) {
667
+ settings.teammateMode = "auto";
818
668
  changed = true;
819
669
  }
820
670
  return changed;
821
671
  }
822
672
 
823
- /**
824
- * Remote Control 자동 활성화.
825
- * 모든 세션에서 remote control URL을 자동 발급하도록 설정.
826
- * @param {object} s - settings 객체 (직접 변경)
827
- * @returns {boolean} 변경 여부
828
- */
829
- function applyRemoteControl(s) {
830
- if (s.remoteControlAtStartup === true) return false;
673
+ function applyRemoteControl(settings) {
674
+ if (settings.remoteControlAtStartup === true) return false;
831
675
  if (process.env.TFX_REMOTE_CONTROL !== "1" && !detectDevMode()) return false;
832
- s.remoteControlAtStartup = true;
676
+ settings.remoteControlAtStartup = true;
833
677
  return true;
834
678
  }
835
679
 
836
- /**
837
- * SessionStart + PreToolUse 훅 섹션 적용.
838
- * @param {object} s - settings 객체 (직접 변경)
839
- * @returns {boolean} 변경 여부
840
- */
841
- function applyHooks(s) {
842
- if (!s.hooks) s.hooks = {};
680
+ function applyHooks(settings) {
681
+ if (!settings.hooks) settings.hooks = {};
843
682
  let changed = false;
844
683
 
845
- // ── SessionStart ──
846
- if (!Array.isArray(s.hooks.SessionStart)) s.hooks.SessionStart = [];
684
+ if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
847
685
 
848
- const hasTrifluxHooks = s.hooks.SessionStart.some((entry) =>
849
- Array.isArray(entry.hooks) &&
850
- entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("triflux")),
686
+ const hasTrifluxHooks = settings.hooks.SessionStart.some(
687
+ (entry) =>
688
+ Array.isArray(entry.hooks) &&
689
+ entry.hooks.some(
690
+ (hook) => typeof hook.command === "string" && hook.command.includes("triflux"),
691
+ ),
851
692
  );
852
693
 
853
694
  if (!hasTrifluxHooks) {
@@ -855,7 +696,7 @@ function applyHooks(s) {
855
696
  const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
856
697
  const pluginRoot = PLUGIN_ROOT.replace(/\\/g, "/");
857
698
 
858
- s.hooks.SessionStart.push({
699
+ settings.hooks.SessionStart.push({
859
700
  matcher: "*",
860
701
  hooks: [
861
702
  {
@@ -878,17 +719,25 @@ function applyHooks(s) {
878
719
  changed = true;
879
720
  }
880
721
 
881
- // ── PreToolUse 훅: headless-guard (auto-route) ──
882
- if (!Array.isArray(s.hooks.PreToolUse)) s.hooks.PreToolUse = [];
883
-
884
- const guardScriptPath = join(CLAUDE_DIR, "scripts", "headless-guard-fast.sh").replace(/\\/g, "/");
885
- const hasGuardHook = s.hooks.PreToolUse.some((entry) =>
886
- Array.isArray(entry.hooks) &&
887
- entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("headless-guard")),
722
+ if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
723
+
724
+ const guardScriptPath = join(
725
+ CLAUDE_DIR,
726
+ "scripts",
727
+ "headless-guard-fast.sh",
728
+ ).replace(/\\/g, "/");
729
+ const hasGuardHook = settings.hooks.PreToolUse.some(
730
+ (entry) =>
731
+ Array.isArray(entry.hooks) &&
732
+ entry.hooks.some(
733
+ (hook) =>
734
+ typeof hook.command === "string" &&
735
+ hook.command.includes("headless-guard"),
736
+ ),
888
737
  );
889
738
 
890
739
  if (!hasGuardHook && existsSync(guardScriptPath.replace(/\//g, "\\"))) {
891
- s.hooks.PreToolUse.push({
740
+ settings.hooks.PreToolUse.push({
892
741
  matcher: "Bash|Agent",
893
742
  hooks: [
894
743
  {
@@ -900,27 +749,38 @@ function applyHooks(s) {
900
749
  });
901
750
  changed = true;
902
751
  } else if (hasGuardHook) {
903
- // 기존 경로를 동기화된 경로로 업데이트
904
- for (const entry of s.hooks.PreToolUse) {
752
+ for (const entry of settings.hooks.PreToolUse) {
905
753
  if (!Array.isArray(entry.hooks)) continue;
906
- for (const h of entry.hooks) {
907
- if (typeof h.command === "string" && h.command.includes("headless-guard") && !h.command.includes(guardScriptPath)) {
908
- h.command = `bash "${guardScriptPath}"`;
754
+ for (const hook of entry.hooks) {
755
+ if (
756
+ typeof hook.command === "string" &&
757
+ hook.command.includes("headless-guard") &&
758
+ !hook.command.includes(guardScriptPath)
759
+ ) {
760
+ hook.command = `bash "${guardScriptPath}"`;
909
761
  changed = true;
910
762
  }
911
763
  }
912
764
  }
913
765
  }
914
766
 
915
- // ── PreToolUse 훅: tfx-gate-activate (Skill 감지 → A+B gate) ──
916
- const gateScriptPath = join(CLAUDE_DIR, "scripts", "tfx-gate-activate.mjs").replace(/\\/g, "/");
917
- const hasGateHook = s.hooks.PreToolUse.some((entry) =>
918
- Array.isArray(entry.hooks) &&
919
- entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("tfx-gate-activate")),
767
+ const gateScriptPath = join(
768
+ CLAUDE_DIR,
769
+ "scripts",
770
+ "tfx-gate-activate.mjs",
771
+ ).replace(/\\/g, "/");
772
+ const hasGateHook = settings.hooks.PreToolUse.some(
773
+ (entry) =>
774
+ Array.isArray(entry.hooks) &&
775
+ entry.hooks.some(
776
+ (hook) =>
777
+ typeof hook.command === "string" &&
778
+ hook.command.includes("tfx-gate-activate"),
779
+ ),
920
780
  );
921
781
 
922
782
  if (!hasGateHook && existsSync(gateScriptPath.replace(/\//g, "\\"))) {
923
- s.hooks.PreToolUse.push({
783
+ settings.hooks.PreToolUse.push({
924
784
  matcher: "Skill",
925
785
  hooks: [
926
786
  {
@@ -932,11 +792,15 @@ function applyHooks(s) {
932
792
  });
933
793
  changed = true;
934
794
  } else if (hasGateHook) {
935
- for (const entry of s.hooks.PreToolUse) {
795
+ for (const entry of settings.hooks.PreToolUse) {
936
796
  if (!Array.isArray(entry.hooks)) continue;
937
- for (const h of entry.hooks) {
938
- if (typeof h.command === "string" && h.command.includes("tfx-gate-activate") && !h.command.includes(gateScriptPath)) {
939
- h.command = `node "${gateScriptPath}"`;
797
+ for (const hook of entry.hooks) {
798
+ if (
799
+ typeof hook.command === "string" &&
800
+ hook.command.includes("tfx-gate-activate") &&
801
+ !hook.command.includes(gateScriptPath)
802
+ ) {
803
+ hook.command = `node "${gateScriptPath}"`;
940
804
  changed = true;
941
805
  }
942
806
  }
@@ -946,220 +810,796 @@ function applyHooks(s) {
946
810
  return changed;
947
811
  }
948
812
 
949
- // 1회 읽기
950
- let settings = {};
951
- if (existsSync(settingsPath)) {
952
- try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch { /* 기존 설정 보존 */ }
953
- }
813
+ function ensureCriticalSetup() {
814
+ const settings = loadSettings();
815
+ let settingsChanged = false;
816
+
817
+ try {
818
+ if (applyStatusLine(settings)) settingsChanged = true;
819
+ } catch {}
820
+ try {
821
+ if (applyAgentTeams(settings)) settingsChanged = true;
822
+ } catch {}
823
+ try {
824
+ if (applyRemoteControl(settings)) settingsChanged = true;
825
+ } catch {}
826
+ try {
827
+ if (applyHooks(settings)) settingsChanged = true;
828
+ } catch {}
954
829
 
955
- // 3개 섹션 일괄 수정 (각각 try-catch로 독립 실행)
956
- let settingsChanged = false;
957
- try { if (applyStatusLine(settings)) { settingsChanged = true; synced++; } } catch {}
958
- try { if (applyAgentTeams(settings)) { settingsChanged = true; synced++; } } catch {}
959
- try { if (applyRemoteControl(settings)) { settingsChanged = true; synced++; } } catch {}
960
- try { if (applyHooks(settings)) { settingsChanged = true; synced++; } } catch {}
830
+ if (settingsChanged) {
831
+ try {
832
+ persistSettings(settings);
833
+ } catch {}
834
+ }
961
835
 
962
- // 1회 쓰기
963
- if (settingsChanged) {
964
836
  try {
965
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
966
- } catch {
967
- // settings.json 쓰기 실패 시 무시
837
+ const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
838
+ const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
839
+ ? readFileSync(BREADCRUMB_PATH, "utf8").trim()
840
+ : "";
841
+ if (currentBreadcrumb !== pkgRootForward) {
842
+ const breadcrumbDir = dirname(BREADCRUMB_PATH);
843
+ if (!existsSync(breadcrumbDir)) {
844
+ mkdirSync(breadcrumbDir, { recursive: true });
845
+ }
846
+ writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
847
+ }
848
+ } catch {}
849
+
850
+ try {
851
+ ensureCodexProfiles();
852
+ } catch {}
853
+ }
854
+
855
+ export {
856
+ BREADCRUMB_PATH,
857
+ CLAUDE_DIR,
858
+ cleanupStaleSkills,
859
+ DEPRECATED_SKILLS,
860
+ detectDevMode,
861
+ ensureCodexHubServerConfig,
862
+ ensureCodexProfiles,
863
+ ensureHooksInSettings,
864
+ extractManagedHookFilename,
865
+ getManagedRegistryHooks,
866
+ getVersion,
867
+ hasProfileSection,
868
+ LEGACY_CODEX_MODELS,
869
+ PLUGIN_ROOT,
870
+ REQUIRED_CODEX_PROFILES,
871
+ readMarker,
872
+ replaceProfileSection,
873
+ SETUP_MARKER_PATH,
874
+ SKILL_ALIASES,
875
+ SYNC_MAP,
876
+ scanHudFiles,
877
+ syncAliasedSkillDir,
878
+ writeMarker,
879
+ };
880
+
881
+ export async function runCritical(stdinData) {
882
+ const io = createCommandIo();
883
+ const argv = getSetupArgv(stdinData);
884
+ const isSync = argv.includes("--sync");
885
+ const isDev = detectDevMode();
886
+
887
+ // version check remains part of the critical path for in-process callers.
888
+ getPackageVersion();
889
+ readMarker();
890
+
891
+ if (isDev) {
892
+ io.log(" [dev] 로컬 개발 모드 감지");
968
893
  }
894
+
895
+ if (isSync) {
896
+ io.log(" [sync] 명시적 재동기화 실행");
897
+ }
898
+
899
+ ensureCriticalSetup();
900
+ return io.result(0);
969
901
  }
970
902
 
971
- // ── HUD 캐시 pre-warm (백그라운드) ──
903
+ export async function runDeferred(stdinData) {
904
+ const io = createCommandIo();
905
+ const argv = getSetupArgv(stdinData);
906
+ const isSync = argv.includes("--sync");
907
+ const isForce = argv.includes("--force");
908
+ const isDev = detectDevMode();
972
909
 
973
- const preWarmHudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
974
- if (existsSync(preWarmHudPath)) {
975
- const refreshFlags = [
976
- ["--refresh-claude-usage"],
977
- ["--refresh-codex-rate-limits"],
978
- ["--refresh-gemini-quota", "--account", "gemini-main"],
979
- ["--refresh-gemini-session"],
980
- ];
981
- for (const args of refreshFlags) {
910
+ if (isDev) {
911
+ io.log(" [dev] \uB85C\uCEEC \uAC1C\uBC1C \uBAA8\uB4DC \uAC10\uC9C0");
912
+ }
913
+
914
+ if (isSync) {
915
+ io.log(
916
+ " [sync] \uBA85\uC2DC\uC801 \uC7AC\uB3D9\uAE30\uD654 \uC2E4\uD589",
917
+ );
918
+ }
919
+
920
+ const pkgVersion = getPackageVersion();
921
+ const marker = readMarker();
922
+ const claudeRoutingResults = syncClaudeRoutingSections();
923
+ const claudeRoutingChangedCount = claudeRoutingResults.filter(
924
+ (result) => result.action === "created" || result.action === "updated",
925
+ ).length;
926
+ if (pkgVersion && marker?.version === pkgVersion && !isForce) {
927
+ if (claudeRoutingChangedCount > 0) {
928
+ io.log(
929
+ `setup: skip core sync (v${pkgVersion} already synced, CLAUDE.md ${claudeRoutingChangedCount}건 반영)`,
930
+ );
931
+ } else {
932
+ io.log(`setup: skip (v${pkgVersion} already synced)`);
933
+ }
934
+ return io.result(0);
935
+ }
936
+
937
+ let synced = claudeRoutingChangedCount;
938
+
939
+ // ── Memory Doctor (P0 자동 수정) ──
940
+ const isCIEnv = process.env.CI === "true" || process.env.DOCKER === "true";
941
+ if (!isCIEnv) {
982
942
  try {
983
- const child = spawn(process.execPath, [preWarmHudPath, ...args], {
984
- detached: true,
985
- stdio: "ignore",
986
- windowsHide: true,
987
- });
988
- child.unref();
989
- } catch { /* pre-warm 실패 무시 */ }
943
+ const { createMemoryDoctor } = await import("../hub/memory-doctor.mjs");
944
+ const projectSlug = process
945
+ .cwd()
946
+ .replace(/^([A-Z]):/u, "$1-")
947
+ .replace(/[\\/]/gu, "-");
948
+ const memDir = join(CLAUDE_DIR, "projects", projectSlug, "memory");
949
+ if (existsSync(memDir)) {
950
+ const doctor = createMemoryDoctor({
951
+ memoryDir: memDir,
952
+ rulesDir: join(process.cwd(), ".claude", "rules"),
953
+ projectDir: process.cwd(),
954
+ claudeDir: CLAUDE_DIR,
955
+ });
956
+ const { checks, healthScore } = doctor.scan();
957
+ const p0Auto = checks.filter(
958
+ (c) => c.severity === "P0" && c.autofix && !c.passed,
959
+ );
960
+ if (p0Auto.length > 0) {
961
+ doctor.fixAll({ severity: "P0" });
962
+ io.log(
963
+ ` memory-doctor: ${p0Auto.length}건 P0 자동 수정 (health: ${healthScore})`,
964
+ );
965
+ synced += p0Auto.length;
966
+ }
967
+ }
968
+ } catch (err) {
969
+ io.log(` memory-doctor: skip (${err.message})`);
970
+ }
990
971
  }
991
- console.log(" \x1b[32m✓\x1b[0m HUD cache pre-warm (background)");
992
- }
993
972
 
994
- // ── Stale PID 파일 정리 (hub 좀비 방지) ──
973
+ for (const { src, dst } of SYNC_MAP) {
974
+ if (!existsSync(src)) continue;
975
+
976
+ const dstDir = dirname(dst);
977
+ if (!existsSync(dstDir)) {
978
+ mkdirSync(dstDir, { recursive: true });
979
+ }
980
+
981
+ if (!existsSync(dst)) {
982
+ copyFileSync(src, dst);
983
+ try {
984
+ chmodSync(dst, 0o755);
985
+ } catch {}
986
+ synced++;
987
+ } else {
988
+ if (shouldSyncTextFile(src, dst)) {
989
+ copyFileSync(src, dst);
990
+ try {
991
+ chmodSync(dst, 0o755);
992
+ } catch {}
993
+ synced++;
994
+ }
995
+ }
996
+ }
995
997
 
996
- const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
997
- if (existsSync(HUB_PID_FILE)) {
998
998
  try {
999
- const pidInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
1000
- process.kill(pidInfo.pid, 0); // 프로세스 존재 확인 (신호 미전송)
1001
- } catch {
1002
- try { unlinkSync(HUB_PID_FILE); } catch {} // 죽은 프로세스면 PID 파일 삭제
1003
- synced++;
999
+ const claudeGuide = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
1000
+ if (claudeGuide.changed) synced++;
1001
+ } catch (e) {
1002
+ io.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 라우팅: ${e.message}`);
1004
1003
  }
1005
- }
1006
1004
 
1007
- // ── psmux 자동 설치 (Windows, headless 모드용) ──
1005
+ // ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
1008
1006
 
1009
- if (process.platform === "win32") {
1010
- try {
1011
- execFileSync("where", ["psmux"], { stdio: "ignore" });
1012
- } catch {
1013
- // psmux 미설치 winget으로 자동 설치 시도
1014
- console.log(" psmux 미설치 winget으로 설치 중...");
1007
+ const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
1008
+ const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
1009
+ const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
1010
+
1011
+ // native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
1012
+ const SKIP_PACKAGES = new Set([
1013
+ "better-sqlite3",
1014
+ "prebuild-install",
1015
+ "node-abi",
1016
+ "node-addon-api",
1017
+ ]);
1018
+
1019
+ if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
1015
1020
  try {
1016
- execFileSync("winget", ["install", "--id", "marlocarlo.psmux", "--accept-package-agreements", "--accept-source-agreements"], {
1017
- stdio: ["ignore", "pipe", "pipe"],
1018
- timeout: 60000,
1019
- });
1020
- console.log(" \x1b[32m✓\x1b[0m psmux 설치 완료");
1021
+ const { cpSync } = await import("fs");
1022
+ for (const entry of readdirSync(srcNodeModules)) {
1023
+ if (SKIP_PACKAGES.has(entry)) continue;
1024
+
1025
+ const src = join(srcNodeModules, entry);
1026
+ const dst = join(workerNodeModules, entry);
1027
+ if (existsSync(dst)) continue;
1028
+
1029
+ mkdirSync(dirname(dst), { recursive: true });
1030
+ cpSync(src, dst, { recursive: true });
1031
+ }
1021
1032
  synced++;
1022
1033
  } catch {
1023
- console.log(" \x1b[33m⚠\x1b[0m psmux 자동 설치 실패 수동 설치: winget install psmux");
1034
+ // best effort: 의존성 복사 실패 exec fallback으로 동작
1024
1035
  }
1025
1036
  }
1026
- }
1027
1037
 
1028
- // ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 시) ──
1038
+ // ── 패키지 루트 breadcrumb 기록 ──
1039
+ // tfx-route.sh가 hub/server.mjs, hub/bridge.mjs를 찾을 수 있도록
1040
+ // 패키지 루트 경로를 ~/.claude/scripts/.tfx-pkg-root에 기록한다.
1041
+ // dev mode에서는 항상 최신 경로를 기록 (--sync 시 강제 갱신).
1042
+ {
1043
+ const pkgRootForward = PLUGIN_ROOT.replace(/\\/g, "/");
1044
+ const currentBreadcrumb = existsSync(BREADCRUMB_PATH)
1045
+ ? readFileSync(BREADCRUMB_PATH, "utf8").trim()
1046
+ : "";
1047
+ if (currentBreadcrumb !== pkgRootForward || isSync) {
1048
+ const breadcrumbDir = dirname(BREADCRUMB_PATH);
1049
+ if (!existsSync(breadcrumbDir))
1050
+ mkdirSync(breadcrumbDir, { recursive: true });
1051
+ writeFileSync(BREADCRUMB_PATH, pkgRootForward + "\n", "utf8");
1052
+ synced++;
1053
+ }
1054
+ }
1029
1055
 
1030
- const cacheDir = join(CLAUDE_DIR, "cache");
1031
- const staleFiles = [
1032
- "claude-usage-cache.json",
1033
- ".claude-refresh-lock",
1034
- "codex-rate-limits-cache.json",
1035
- ];
1056
+ // ── 에이전트 동기화 (.claude/agents/ → ~/.claude/agents/) ──
1057
+ // slim-wrapper 커스텀 에이전트를 글로벌에 배포하여
1058
+ // 다른 프로젝트에서도 subagent_type으로 참조 가능하게 한다.
1059
+
1060
+ const agentsSrc = join(PLUGIN_ROOT, ".claude", "agents");
1061
+ const agentsDst = join(CLAUDE_DIR, "agents");
1062
+
1063
+ if (existsSync(agentsSrc)) {
1064
+ if (!existsSync(agentsDst)) mkdirSync(agentsDst, { recursive: true });
1065
+
1066
+ for (const name of readdirSync(agentsSrc)) {
1067
+ if (!name.endsWith(".md")) continue;
1068
+
1069
+ const src = join(agentsSrc, name);
1070
+ const dst = join(agentsDst, name);
1071
+
1072
+ if (!existsSync(dst)) {
1073
+ copyFileSync(src, dst);
1074
+ synced++;
1075
+ } else if (shouldSyncTextFile(src, dst)) {
1076
+ copyFileSync(src, dst);
1077
+ synced++;
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ // ── 스킬 동기화 ──
1083
+ // SKILL.md + 하위 디렉토리(references/ 등)를 재귀적으로 동기화
1084
+
1085
+ const skillsSrc = join(PLUGIN_ROOT, "skills");
1086
+ const skillsDst = join(CLAUDE_DIR, "skills");
1036
1087
 
1037
- for (const name of staleFiles) {
1038
- const fp = join(cacheDir, name);
1039
- if (!existsSync(fp)) continue;
1088
+ function syncSkillDir(srcDir, dstDir) {
1089
+ if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
1090
+
1091
+ let count = 0;
1092
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
1093
+ const srcPath = join(srcDir, entry.name);
1094
+ const dstPath = join(dstDir, entry.name);
1095
+
1096
+ if (entry.isDirectory()) {
1097
+ count += syncSkillDir(srcPath, dstPath);
1098
+ } else if (entry.name.endsWith(".md")) {
1099
+ if (shouldSyncTextFile(srcPath, dstPath)) {
1100
+ copyFileSync(srcPath, dstPath);
1101
+ count++;
1102
+ }
1103
+ }
1104
+ }
1105
+ return count;
1106
+ }
1107
+
1108
+ if (existsSync(skillsSrc)) {
1109
+ for (const name of readdirSync(skillsSrc)) {
1110
+ const skillDir = join(skillsSrc, name);
1111
+ const skillMd = join(skillDir, "SKILL.md");
1112
+ if (!existsSync(skillMd)) continue;
1113
+
1114
+ synced += syncSkillDir(skillDir, join(skillsDst, name));
1115
+ }
1116
+ }
1117
+
1118
+ // ── settings.json 통합 R/W ──
1119
+ // 3개 섹션(statusLine, agentTeams, hooks)을 1회 read → 일괄 수정 → 1회 write
1120
+
1121
+ const settingsPath = join(CLAUDE_DIR, "settings.json");
1122
+ const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
1123
+
1124
+ /**
1125
+ * statusLine 섹션 적용.
1126
+ * @param {object} s - settings 객체 (직접 변경)
1127
+ * @returns {boolean} 변경 여부
1128
+ */
1129
+ function applyStatusLine(s) {
1130
+ if (!existsSync(hudPath)) return false;
1131
+ const currentCmd = s.statusLine?.command || "";
1132
+ if (currentCmd.includes("hud-qos-status.mjs")) return false;
1133
+
1134
+ const nodePath = process.execPath.replace(/\\/g, "/");
1135
+ const hudForward = hudPath.replace(/\\/g, "/");
1136
+ const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
1137
+ const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
1138
+
1139
+ s.statusLine = { type: "command", command: `${nodeRef} ${hudRef}` };
1140
+ return true;
1141
+ }
1142
+
1143
+ /**
1144
+ * Agent Teams 환경변수 섹션 적용.
1145
+ * @param {object} s - settings 객체 (직접 변경)
1146
+ * @returns {boolean} 변경 여부
1147
+ */
1148
+ function applyAgentTeams(s) {
1149
+ if (!s.env) s.env = {};
1150
+ let changed = false;
1151
+
1152
+ if (s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
1153
+ s.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
1154
+ changed = true;
1155
+ }
1156
+ // teammateMode: auto (tmux 밖이면 in-process, 안이면 split-pane)
1157
+ if (!s.teammateMode) {
1158
+ s.teammateMode = "auto";
1159
+ changed = true;
1160
+ }
1161
+ return changed;
1162
+ }
1163
+
1164
+ /**
1165
+ * Remote Control 자동 활성화.
1166
+ * 모든 세션에서 remote control URL을 자동 발급하도록 설정.
1167
+ * @param {object} s - settings 객체 (직접 변경)
1168
+ * @returns {boolean} 변경 여부
1169
+ */
1170
+ function applyRemoteControl(s) {
1171
+ if (s.remoteControlAtStartup === true) return false;
1172
+ if (process.env.TFX_REMOTE_CONTROL !== "1" && !detectDevMode())
1173
+ return false;
1174
+ s.remoteControlAtStartup = true;
1175
+ return true;
1176
+ }
1177
+
1178
+ /**
1179
+ * SessionStart + PreToolUse 훅 섹션 적용.
1180
+ * @param {object} s - settings 객체 (직접 변경)
1181
+ * @returns {boolean} 변경 여부
1182
+ */
1183
+ function applyHooks(s) {
1184
+ if (!s.hooks) s.hooks = {};
1185
+ let changed = false;
1186
+
1187
+ // ── SessionStart 훅 ──
1188
+ if (!Array.isArray(s.hooks.SessionStart)) s.hooks.SessionStart = [];
1189
+
1190
+ const hasTrifluxHooks = s.hooks.SessionStart.some(
1191
+ (entry) =>
1192
+ Array.isArray(entry.hooks) &&
1193
+ entry.hooks.some(
1194
+ (h) => typeof h.command === "string" && h.command.includes("triflux"),
1195
+ ),
1196
+ );
1197
+
1198
+ if (!hasTrifluxHooks) {
1199
+ const nodePath = process.execPath.replace(/\\/g, "/");
1200
+ const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
1201
+ const pluginRoot = PLUGIN_ROOT.replace(/\\/g, "/");
1202
+
1203
+ s.hooks.SessionStart.push({
1204
+ matcher: "*",
1205
+ hooks: [
1206
+ {
1207
+ type: "command",
1208
+ command: `${nodeRef} "${pluginRoot}/scripts/setup.mjs"`,
1209
+ timeout: 10,
1210
+ },
1211
+ {
1212
+ type: "command",
1213
+ command: `${nodeRef} "${pluginRoot}/scripts/hub-ensure.mjs"`,
1214
+ timeout: 8,
1215
+ },
1216
+ {
1217
+ type: "command",
1218
+ command: `${nodeRef} "${pluginRoot}/scripts/preflight-cache.mjs"`,
1219
+ timeout: 5,
1220
+ },
1221
+ ],
1222
+ });
1223
+ changed = true;
1224
+ }
1225
+
1226
+ // ── PreToolUse 훅: headless-guard (auto-route) ──
1227
+ if (!Array.isArray(s.hooks.PreToolUse)) s.hooks.PreToolUse = [];
1228
+
1229
+ const guardScriptPath = join(
1230
+ CLAUDE_DIR,
1231
+ "scripts",
1232
+ "headless-guard-fast.sh",
1233
+ ).replace(/\\/g, "/");
1234
+ const hasGuardHook = s.hooks.PreToolUse.some(
1235
+ (entry) =>
1236
+ Array.isArray(entry.hooks) &&
1237
+ entry.hooks.some(
1238
+ (h) =>
1239
+ typeof h.command === "string" &&
1240
+ h.command.includes("headless-guard"),
1241
+ ),
1242
+ );
1243
+
1244
+ if (!hasGuardHook && existsSync(guardScriptPath.replace(/\//g, "\\"))) {
1245
+ s.hooks.PreToolUse.push({
1246
+ matcher: "Bash|Agent",
1247
+ hooks: [
1248
+ {
1249
+ type: "command",
1250
+ command: `bash "${guardScriptPath}"`,
1251
+ timeout: 3,
1252
+ },
1253
+ ],
1254
+ });
1255
+ changed = true;
1256
+ } else if (hasGuardHook) {
1257
+ // 기존 훅 경로를 동기화된 경로로 업데이트
1258
+ for (const entry of s.hooks.PreToolUse) {
1259
+ if (!Array.isArray(entry.hooks)) continue;
1260
+ for (const h of entry.hooks) {
1261
+ if (
1262
+ typeof h.command === "string" &&
1263
+ h.command.includes("headless-guard") &&
1264
+ !h.command.includes(guardScriptPath)
1265
+ ) {
1266
+ h.command = `bash "${guardScriptPath}"`;
1267
+ changed = true;
1268
+ }
1269
+ }
1270
+ }
1271
+ }
1272
+
1273
+ // ── PreToolUse 훅: tfx-gate-activate (Skill 감지 → A+B gate) ──
1274
+ const gateScriptPath = join(
1275
+ CLAUDE_DIR,
1276
+ "scripts",
1277
+ "tfx-gate-activate.mjs",
1278
+ ).replace(/\\/g, "/");
1279
+ const hasGateHook = s.hooks.PreToolUse.some(
1280
+ (entry) =>
1281
+ Array.isArray(entry.hooks) &&
1282
+ entry.hooks.some(
1283
+ (h) =>
1284
+ typeof h.command === "string" &&
1285
+ h.command.includes("tfx-gate-activate"),
1286
+ ),
1287
+ );
1288
+
1289
+ if (!hasGateHook && existsSync(gateScriptPath.replace(/\//g, "\\"))) {
1290
+ s.hooks.PreToolUse.push({
1291
+ matcher: "Skill",
1292
+ hooks: [
1293
+ {
1294
+ type: "command",
1295
+ command: `node "${gateScriptPath}"`,
1296
+ timeout: 2,
1297
+ },
1298
+ ],
1299
+ });
1300
+ changed = true;
1301
+ } else if (hasGateHook) {
1302
+ for (const entry of s.hooks.PreToolUse) {
1303
+ if (!Array.isArray(entry.hooks)) continue;
1304
+ for (const h of entry.hooks) {
1305
+ if (
1306
+ typeof h.command === "string" &&
1307
+ h.command.includes("tfx-gate-activate") &&
1308
+ !h.command.includes(gateScriptPath)
1309
+ ) {
1310
+ h.command = `node "${gateScriptPath}"`;
1311
+ changed = true;
1312
+ }
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ return changed;
1318
+ }
1319
+
1320
+ // 1회 읽기
1321
+ let settings = {};
1322
+ if (existsSync(settingsPath)) {
1323
+ try {
1324
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
1325
+ } catch {
1326
+ /* 기존 설정 보존 */
1327
+ }
1328
+ }
1329
+
1330
+ // 3개 섹션 일괄 수정 (각각 try-catch로 독립 실행)
1331
+ let settingsChanged = false;
1040
1332
  try {
1041
- const content = readFileSync(fp, "utf8");
1042
- const parsed = JSON.parse(content);
1043
- // 에러 상태이거나 락 파일이면 삭제 → 새 세션에서 fresh start
1044
- if (parsed.error || name.startsWith(".")) {
1045
- unlinkSync(fp);
1333
+ if (applyStatusLine(settings)) {
1334
+ settingsChanged = true;
1046
1335
  synced++;
1047
1336
  }
1048
- } catch {
1049
- // 파싱 실패 파일도 삭제
1050
- try { unlinkSync(fp); } catch {}
1051
- }
1052
- }
1337
+ } catch {}
1338
+ try {
1339
+ if (applyAgentTeams(settings)) {
1340
+ settingsChanged = true;
1341
+ synced++;
1342
+ }
1343
+ } catch {}
1344
+ try {
1345
+ if (applyRemoteControl(settings)) {
1346
+ settingsChanged = true;
1347
+ synced++;
1348
+ }
1349
+ } catch {}
1350
+ try {
1351
+ if (applyHooks(settings)) {
1352
+ settingsChanged = true;
1353
+ synced++;
1354
+ }
1355
+ } catch {}
1053
1356
 
1054
- // ── Windows bash PATH 자동 설정 ──
1055
- // Codex/Gemini가 cmd에는 있지만 bash에서 못 찾는 문제 해결
1357
+ // 1회 쓰기
1358
+ if (settingsChanged) {
1359
+ try {
1360
+ writeFileSync(
1361
+ settingsPath,
1362
+ JSON.stringify(settings, null, 2) + "\n",
1363
+ "utf8",
1364
+ );
1365
+ } catch {
1366
+ // settings.json 쓰기 실패 시 무시
1367
+ }
1368
+ }
1056
1369
 
1057
- if (process.platform === "win32") {
1058
- const npmBin = join(process.env.APPDATA || "", "npm");
1059
- if (existsSync(npmBin)) {
1060
- const bashrcPath = join(homedir(), ".bashrc");
1061
- const pathExport = 'export PATH="$PATH:$APPDATA/npm"';
1062
- let needsUpdate = true;
1370
+ // ── HUD 캐시 pre-warm (백그라운드) ──
1063
1371
 
1064
- if (existsSync(bashrcPath)) {
1065
- const content = readFileSync(bashrcPath, "utf8");
1066
- if (content.includes("APPDATA/npm") || content.includes("APPDATA\\npm")) {
1067
- needsUpdate = false;
1372
+ const preWarmHudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
1373
+ if (existsSync(preWarmHudPath)) {
1374
+ const refreshFlags = [
1375
+ ["--refresh-claude-usage"],
1376
+ ["--refresh-codex-rate-limits"],
1377
+ ["--refresh-gemini-quota", "--account", "gemini-main"],
1378
+ ["--refresh-gemini-session"],
1379
+ ];
1380
+ for (const args of refreshFlags) {
1381
+ try {
1382
+ const child = spawn(process.execPath, [preWarmHudPath, ...args], {
1383
+ detached: true,
1384
+ stdio: "ignore",
1385
+ windowsHide: true,
1386
+ });
1387
+ child.unref();
1388
+ } catch {
1389
+ /* pre-warm 실패 무시 */
1068
1390
  }
1069
1391
  }
1392
+ io.log(" \x1b[32m✓\x1b[0m HUD cache pre-warm (background)");
1393
+ }
1394
+
1395
+ // ── Stale PID 파일 정리 (hub 좀비 방지) ──
1396
+
1397
+ const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
1398
+ if (existsSync(HUB_PID_FILE)) {
1399
+ try {
1400
+ const pidInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
1401
+ process.kill(pidInfo.pid, 0); // 프로세스 존재 확인 (신호 미전송)
1402
+ } catch {
1403
+ try {
1404
+ unlinkSync(HUB_PID_FILE);
1405
+ } catch {} // 죽은 프로세스면 PID 파일 삭제
1406
+ synced++;
1407
+ }
1408
+ }
1409
+
1410
+ // ── psmux 자동 설치 (Windows, headless 모드용) ──
1070
1411
 
1071
- if (needsUpdate) {
1072
- const line = `\n# triflux: Codex/Gemini CLI를 bash에서 사용하기 위한 PATH 설정\n${pathExport}\n`;
1412
+ if (process.platform === "win32") {
1413
+ try {
1414
+ execFileSync("where", ["psmux"], { stdio: "ignore" });
1415
+ } catch {
1416
+ // psmux 미설치 — winget으로 자동 설치 시도
1417
+ io.log(" psmux 미설치 — winget으로 설치 중...");
1073
1418
  try {
1074
- writeFileSync(bashrcPath, (existsSync(bashrcPath) ? readFileSync(bashrcPath, "utf8") : "") + line, "utf8");
1419
+ execFileSync(
1420
+ "winget",
1421
+ [
1422
+ "install",
1423
+ "--id",
1424
+ "marlocarlo.psmux",
1425
+ "--accept-package-agreements",
1426
+ "--accept-source-agreements",
1427
+ ],
1428
+ {
1429
+ stdio: ["ignore", "pipe", "pipe"],
1430
+ timeout: 60000,
1431
+ },
1432
+ );
1433
+ io.log(" \x1b[32m✓\x1b[0m psmux 설치 완료");
1075
1434
  synced++;
1076
- } catch {}
1435
+ } catch {
1436
+ io.log(
1437
+ " \x1b[33m⚠\x1b[0m psmux 자동 설치 실패 — 수동 설치: winget install psmux",
1438
+ );
1439
+ }
1077
1440
  }
1078
1441
  }
1079
- }
1080
1442
 
1081
- // ── Codex 프로필 자동 보정 ──
1443
+ // ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 시) ──
1082
1444
 
1083
- const codexProfilesResult = ensureCodexProfiles();
1084
- if (codexProfilesResult.ok && codexProfilesResult.changed > 0) {
1085
- synced++;
1086
- }
1445
+ const cacheDir = join(CLAUDE_DIR, "cache");
1446
+ const staleFiles = [
1447
+ "claude-usage-cache.json",
1448
+ ".claude-refresh-lock",
1449
+ "codex-rate-limits-cache.json",
1450
+ ];
1451
+
1452
+ for (const name of staleFiles) {
1453
+ const fp = join(cacheDir, name);
1454
+ if (!existsSync(fp)) continue;
1455
+ try {
1456
+ const content = readFileSync(fp, "utf8");
1457
+ const parsed = JSON.parse(content);
1458
+ // 에러 상태이거나 락 파일이면 삭제 → 새 세션에서 fresh start
1459
+ if (parsed.error || name.startsWith(".")) {
1460
+ unlinkSync(fp);
1461
+ synced++;
1462
+ }
1463
+ } catch {
1464
+ // 파싱 실패 파일도 삭제
1465
+ try {
1466
+ unlinkSync(fp);
1467
+ } catch {}
1468
+ }
1469
+ }
1087
1470
 
1088
- // ── CLAUDE.md 라우팅 섹션 자동 동기화 ──
1471
+ // ── Windows bash PATH 자동 설정 ──
1472
+ // Codex/Gemini가 cmd에는 있지만 bash에서 못 찾는 문제 해결
1473
+
1474
+ if (process.platform === "win32") {
1475
+ const npmBin = join(process.env.APPDATA || "", "npm");
1476
+ if (existsSync(npmBin)) {
1477
+ const bashrcPath = join(homedir(), ".bashrc");
1478
+ const pathExport = 'export PATH="$PATH:$APPDATA/npm"';
1479
+ let needsUpdate = true;
1480
+
1481
+ if (existsSync(bashrcPath)) {
1482
+ const content = readFileSync(bashrcPath, "utf8");
1483
+ if (
1484
+ content.includes("APPDATA/npm") ||
1485
+ content.includes("APPDATA\\npm")
1486
+ ) {
1487
+ needsUpdate = false;
1488
+ }
1489
+ }
1089
1490
 
1090
- try {
1091
- const routingTable = getLatestRoutingTable();
1092
- const projectResult = ensureTfxSection(join(PLUGIN_ROOT, "CLAUDE.md"), routingTable);
1093
- if (projectResult.action !== "unchanged") {
1094
- console.log(` \x1b[32m✓\x1b[0m CLAUDE.md (project): ${projectResult.action}`);
1095
- synced++;
1491
+ if (needsUpdate) {
1492
+ const line = `\n# triflux: Codex/Gemini CLI를 bash에서 사용하기 위한 PATH 설정\n${pathExport}\n`;
1493
+ try {
1494
+ writeFileSync(
1495
+ bashrcPath,
1496
+ (existsSync(bashrcPath) ? readFileSync(bashrcPath, "utf8") : "") +
1497
+ line,
1498
+ "utf8",
1499
+ );
1500
+ synced++;
1501
+ } catch {}
1502
+ }
1503
+ }
1096
1504
  }
1097
- const globalResult = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
1098
- if (globalResult.action !== "unchanged") {
1099
- console.log(` \x1b[32m✓\x1b[0m CLAUDE.md (global): ${globalResult.action}`);
1505
+
1506
+ // ── Codex 프로필 자동 보정 ──
1507
+
1508
+ const codexProfilesResult = ensureCodexProfiles();
1509
+ if (codexProfilesResult.ok && codexProfilesResult.changed > 0) {
1100
1510
  synced++;
1101
1511
  }
1102
- } catch (error) {
1103
- console.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 동기화 실패: ${error.message}`);
1104
- }
1105
- // ── MCP 인벤토리 백그라운드 갱신 ──
1106
-
1107
- const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
1108
- if (existsSync(mcpCheck)) {
1109
- const child = spawn(process.execPath, [mcpCheck], {
1110
- detached: true,
1111
- stdio: "ignore",
1112
- windowsHide: true,
1113
- });
1114
- child.unref(); // 부모 프로세스와 분리 — 비동기 실행
1115
- }
1116
1512
 
1117
- // ── /tmp 임시 파일 자동 정리 (setup 지연 방지: fire-and-forget) ──
1118
- cleanupTmpFiles().catch(() => {});
1513
+ // ── CLAUDE.md 라우팅 섹션 자동 동기화 ──
1119
1514
 
1120
- // ── npm 글로벌 패키지 동기화 ──
1121
- // dev mode가 아닌 경우(npm install로 설치), 글로벌 triflux 패키지 버전을 확인하고
1122
- // 로컬 버전과 다르면 업데이트를 안내한다. dev mode에서는 git 기반이므로 skip.
1123
- if (pkgVersion && !isDev) {
1124
1515
  try {
1125
- const globalVer = execFileSync("npm", ["list", "-g", "triflux", "--json", "--depth=0"], {
1126
- encoding: "utf8",
1127
- timeout: 10000,
1128
- stdio: ["pipe", "pipe", "pipe"],
1129
- });
1130
- const parsed = JSON.parse(globalVer);
1131
- const installedVer = parsed?.dependencies?.triflux?.version;
1132
- if (installedVer && installedVer !== pkgVersion) {
1133
- const tag = pkgVersion.includes("alpha") ? "alpha" : "latest";
1134
- console.log(` npm: triflux global ${installedVer} → ${pkgVersion} (npm i -g triflux@${tag})`);
1516
+ const routingTable = getLatestRoutingTable();
1517
+ const projectResult = ensureTfxSection(
1518
+ join(PLUGIN_ROOT, "CLAUDE.md"),
1519
+ routingTable,
1520
+ );
1521
+ if (projectResult.action !== "unchanged") {
1522
+ io.log(
1523
+ ` \x1b[32m✓\x1b[0m CLAUDE.md (project): ${projectResult.action}`,
1524
+ );
1525
+ synced++;
1135
1526
  }
1136
- } catch {
1137
- // npm list 실패 = 글로벌 미설치. 안내만 출력.
1138
- if (pkgVersion.includes("alpha")) {
1139
- console.log(" npm: triflux global 미설치 (npm i -g triflux@alpha 로 설치 가능)");
1527
+ const globalResult = ensureGlobalClaudeRoutingSection(CLAUDE_DIR);
1528
+ if (globalResult.action !== "unchanged") {
1529
+ io.log(
1530
+ ` \x1b[32m✓\x1b[0m CLAUDE.md (global): ${globalResult.action}`,
1531
+ );
1532
+ synced++;
1140
1533
  }
1534
+ } catch (error) {
1535
+ io.log(` \x1b[33m⚠\x1b[0m CLAUDE.md 동기화 실패: ${error.message}`);
1536
+ }
1537
+ // ── MCP 인벤토리 백그라운드 갱신 ──
1538
+
1539
+ const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
1540
+ if (existsSync(mcpCheck)) {
1541
+ const child = spawn(process.execPath, [mcpCheck], {
1542
+ detached: true,
1543
+ stdio: "ignore",
1544
+ windowsHide: true,
1545
+ });
1546
+ child.unref(); // 부모 프로세스와 분리 — 비동기 실행
1141
1547
  }
1142
- }
1143
1548
 
1144
- if (pkgVersion) {
1145
- writeMarker({ version: pkgVersion, timestamp: Date.now() });
1146
- }
1549
+ // ── /tmp 임시 파일 자동 정리 (setup 지연 방지: fire-and-forget) ──
1550
+ cleanupTmpFiles().catch(() => {});
1551
+
1552
+ // ── npm 글로벌 패키지 동기화 ──
1553
+ // dev mode가 아닌 경우(npm install로 설치), 글로벌 triflux 패키지 버전을 확인하고
1554
+ // 로컬 버전과 다르면 업데이트를 안내한다. dev mode에서는 git 기반이므로 skip.
1555
+ if (pkgVersion && !isDev) {
1556
+ try {
1557
+ const globalVer = execFileSync(
1558
+ "npm",
1559
+ ["list", "-g", "triflux", "--json", "--depth=0"],
1560
+ {
1561
+ encoding: "utf8",
1562
+ timeout: 10000,
1563
+ stdio: ["pipe", "pipe", "pipe"],
1564
+ },
1565
+ );
1566
+ const parsed = JSON.parse(globalVer);
1567
+ const installedVer = parsed?.dependencies?.triflux?.version;
1568
+ if (installedVer && installedVer !== pkgVersion) {
1569
+ const tag = pkgVersion.includes("alpha") ? "alpha" : "latest";
1570
+ io.log(
1571
+ ` npm: triflux global ${installedVer} → ${pkgVersion} (npm i -g triflux@${tag})`,
1572
+ );
1573
+ }
1574
+ } catch {
1575
+ // npm list 실패 = 글로벌 미설치. 안내만 출력.
1576
+ if (pkgVersion.includes("alpha")) {
1577
+ io.log(
1578
+ " npm: triflux global 미설치 (npm i -g triflux@alpha 로 설치 가능)",
1579
+ );
1580
+ }
1581
+ }
1582
+ }
1147
1583
 
1148
- // ── postinstall 배너 (npm install 시에만 출력) ──
1584
+ if (pkgVersion) {
1585
+ writeMarker({ version: pkgVersion, timestamp: Date.now() });
1586
+ }
1149
1587
 
1150
- if (process.env.npm_lifecycle_event === "postinstall") {
1151
- const G = "\x1b[32m";
1152
- const C = "\x1b[36m";
1153
- const Y = "\x1b[33m";
1154
- const D = "\x1b[2m";
1155
- const B = "\x1b[1m";
1156
- const R = "\x1b[0m";
1588
+ // ── postinstall 배너 (npm install 시에만 출력) ──
1157
1589
 
1158
- const ver = (() => {
1159
- return pkgVersion || "?";
1160
- })();
1590
+ if (process.env.npm_lifecycle_event === "postinstall") {
1591
+ const G = "\x1b[32m";
1592
+ const C = "\x1b[36m";
1593
+ const Y = "\x1b[33m";
1594
+ const D = "\x1b[2m";
1595
+ const B = "\x1b[1m";
1596
+ const R = "\x1b[0m";
1161
1597
 
1162
- console.log(`
1598
+ const ver = (() => {
1599
+ return pkgVersion || "?";
1600
+ })();
1601
+
1602
+ io.log(`
1163
1603
  ${B}╔═══════════════════════════════════════════════╗${R}
1164
1604
  ${B}║${R} ${C}triflux${R} ${D}v${ver}${R} ${B}— Setup Complete${R} ${B}║${R}
1165
1605
  ${B}╚═══════════════════════════════════════════════╝${R}
@@ -1191,11 +1631,20 @@ ${B}Skills (Claude Code):${R}
1191
1631
  ${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
1192
1632
  ${D}https://github.com/tellang/triflux${R}
1193
1633
  `);
1194
- }
1634
+ }
1195
1635
 
1196
- process.exit(0);
1636
+ return io.result(0);
1197
1637
  }
1198
1638
 
1199
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
1200
- main();
1639
+ const isMain =
1640
+ process.argv[1] &&
1641
+ import.meta.url.endsWith(
1642
+ process.argv[1].replace(/\\/g, "/").split("/").pop(),
1643
+ );
1644
+
1645
+ if (isMain) {
1646
+ const result = await runDeferred({ argv: process.argv.slice(2) });
1647
+ if (result.stdout) process.stdout.write(result.stdout);
1648
+ if (result.stderr) process.stderr.write(result.stderr);
1649
+ process.exit(result.code);
1201
1650
  }