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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  // hooks/hook-orchestrator.mjs — 범용 훅 체이닝 엔진
3
4
  //
4
5
  // settings.json에 이벤트당 하나만 등록. stdin JSON에서 이벤트명+툴명을 읽고
@@ -19,10 +20,11 @@
19
20
  // CLAUDE_PLUGIN_ROOT — ${PLUGIN_ROOT} 치환용
20
21
  // HOME / USERPROFILE — ${HOME} 치환용
21
22
 
22
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
23
- import { join, dirname } from "node:path";
23
+ import { execFile, execFileSync } from "node:child_process";
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import { dirname, join } from "node:path";
24
27
  import { fileURLToPath } from "node:url";
25
- import { execFileSync, execFile } from "node:child_process";
26
28
  import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
27
29
 
28
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -84,7 +86,8 @@ function executeHook(hook, stdinData) {
84
86
  // "bash script.sh" → ["bash", ["script.sh"]]
85
87
  // 따옴표 처리 포함
86
88
  const parts = parseCommand(cmd);
87
- if (parts.length === 0) return { code: 1, stdout: "", stderr: "empty command" };
89
+ if (parts.length === 0)
90
+ return { code: 1, stdout: "", stderr: "empty command" };
88
91
 
89
92
  const [executable, ...args] = parts;
90
93
 
@@ -114,20 +117,35 @@ function executeHookAsync(hook, stdinData) {
114
117
  const cmd = resolveCommand(hook.command);
115
118
  const timeout = (hook.timeout || 10) * 1000;
116
119
  const parts = parseCommand(cmd);
117
- if (parts.length === 0) { resolve({ code: 1, stdout: "", stderr: "empty command" }); return; }
120
+ if (parts.length === 0) {
121
+ resolve({ code: 1, stdout: "", stderr: "empty command" });
122
+ return;
123
+ }
118
124
  const [executable, ...args] = parts;
119
- const child = execFile(executable, args, {
120
- timeout,
121
- encoding: "utf8",
122
- windowsHide: true,
123
- cwd: process.cwd(),
124
- env: { ...process.env },
125
- }, (err, stdout, stderr) => {
126
- if (err) resolve({ code: err.status ?? 1, stdout: stdout || "", stderr: stderr || "" });
127
- else resolve({ code: 0, stdout: stdout || "", stderr: "" });
128
- });
129
- if (stdinData) { child.stdin.write(stdinData); child.stdin.end(); }
130
- else child.stdin.end();
125
+ const child = execFile(
126
+ executable,
127
+ args,
128
+ {
129
+ timeout,
130
+ encoding: "utf8",
131
+ windowsHide: true,
132
+ cwd: process.cwd(),
133
+ env: { ...process.env },
134
+ },
135
+ (err, stdout, stderr) => {
136
+ if (err)
137
+ resolve({
138
+ code: err.status ?? 1,
139
+ stdout: stdout || "",
140
+ stderr: stderr || "",
141
+ });
142
+ else resolve({ code: 0, stdout: stdout || "", stderr: "" });
143
+ },
144
+ );
145
+ if (stdinData) {
146
+ child.stdin.write(stdinData);
147
+ child.stdin.end();
148
+ } else child.stdin.end();
131
149
  });
132
150
  }
133
151
 
@@ -170,11 +188,19 @@ function mergeOutputs(accumulated, newOutput) {
170
188
 
171
189
  // hookSpecificOutput 머지 — additionalContext는 누적, 나머지는 덮어쓰기
172
190
  if (parsed.hookSpecificOutput) {
173
- if (accumulated.hookSpecificOutput?.additionalContext && parsed.hookSpecificOutput.additionalContext) {
191
+ if (
192
+ accumulated.hookSpecificOutput?.additionalContext &&
193
+ parsed.hookSpecificOutput.additionalContext
194
+ ) {
174
195
  parsed.hookSpecificOutput.additionalContext =
175
- accumulated.hookSpecificOutput.additionalContext + "\n" + parsed.hookSpecificOutput.additionalContext;
196
+ accumulated.hookSpecificOutput.additionalContext +
197
+ "\n" +
198
+ parsed.hookSpecificOutput.additionalContext;
176
199
  }
177
- accumulated.hookSpecificOutput = { ...accumulated.hookSpecificOutput, ...parsed.hookSpecificOutput };
200
+ accumulated.hookSpecificOutput = {
201
+ ...accumulated.hookSpecificOutput,
202
+ ...parsed.hookSpecificOutput,
203
+ };
178
204
  }
179
205
  // systemMessage는 누적
180
206
  if (parsed.systemMessage) {
@@ -220,7 +246,12 @@ function recordRouteOutcome(slug, mode, outcome) {
220
246
 
221
247
  const weights = existsSync(weightsPath)
222
248
  ? JSON.parse(readFileSync(weightsPath, "utf8"))
223
- : { updated_at: null, total_routes: 0, overrides: 0, weights: { mode_bias: {}, profile_bias: {}, depth_bias: {} } };
249
+ : {
250
+ updated_at: null,
251
+ total_routes: 0,
252
+ overrides: 0,
253
+ weights: { mode_bias: {}, profile_bias: {}, depth_bias: {} },
254
+ };
224
255
 
225
256
  weights.total_routes++;
226
257
  weights.updated_at = new Date().toISOString();
@@ -284,6 +315,32 @@ async function main() {
284
315
 
285
316
  if (!eventName) process.exit(0);
286
317
 
318
+ // ── SessionStart fast-path ──
319
+ // TRIFLUX_HOOK_FAST_PATH=false로 비활성화 가능 (rollback)
320
+ if (eventName === "SessionStart" && process.env.TRIFLUX_HOOK_FAST_PATH !== "false") {
321
+ try {
322
+ const { execute } = await import("./session-start-fast.mjs");
323
+ const result = await execute(stdinRaw);
324
+ if (result.stdout?.trim()) {
325
+ const output = { additionalContext: result.stdout.trim() };
326
+ process.stdout.write(JSON.stringify(output));
327
+ }
328
+
329
+ // external source 훅 (session-vault 등)은 기존 방식으로 실행
330
+ const allHooks = registry.events.SessionStart || [];
331
+ const externalHooks = allHooks.filter((h) => h.enabled !== false && h.source !== "triflux");
332
+ for (const hook of externalHooks) {
333
+ const hookResult = executeHookAsync(hook, stdinRaw);
334
+ hookResult.catch(() => {}); // fire-and-forget for external hooks
335
+ }
336
+
337
+ process.exit(0);
338
+ } catch (err) {
339
+ // fast-path 실패 시 기존 방식으로 폴백
340
+ process.stderr.write(`[orchestrator] fast-path failed, falling back: ${err.message}\n`);
341
+ }
342
+ }
343
+
287
344
  // 이벤트에 해당하는 훅 목록
288
345
  const hooks = registry.events[eventName];
289
346
  if (!hooks || hooks.length === 0) process.exit(0);
@@ -326,7 +383,9 @@ async function main() {
326
383
  }
327
384
  } else {
328
385
  // 같은 priority 다중 훅 — 비동기 병렬 실행
329
- const results = await Promise.all(group.hooks.map((h) => executeHookAsync(h, stdinRaw)));
386
+ const results = await Promise.all(
387
+ group.hooks.map((h) => executeHookAsync(h, stdinRaw)),
388
+ );
330
389
  for (const result of results) {
331
390
  if (result.code === 2) {
332
391
  if (result.stderr) process.stderr.write(result.stderr);
@@ -340,18 +399,43 @@ async function main() {
340
399
  }
341
400
  }
342
401
 
402
+ // ── PostToolUse: 컨텍스트 압축 nudge (인라인, 프로세스 추가 없음) ──
403
+ if (eventName === "PostToolUse" && !blocked) {
404
+ try {
405
+ const home = process.env.HOME || process.env.USERPROFILE || "";
406
+ const snapshotPath = join(home, ".claude", "cache", "tfx-hub", "context-monitor.json");
407
+ const nudgeMarker = join(tmpdir(), "tfx-compact-nudge-sent");
408
+ if (existsSync(snapshotPath) && !existsSync(nudgeMarker)) {
409
+ const snap = JSON.parse(readFileSync(snapshotPath, "utf8"));
410
+ const percent = Number(snap.percent || 0);
411
+ if (percent >= 80) {
412
+ const level = percent >= 90 ? "critical" : "warn";
413
+ const msg = level === "critical"
414
+ ? `[context ${percent}%] 컨텍스트 ${percent}% 사용. /compact 또는 에이전트 분할을 강력 권장합니다.`
415
+ : `[context ${percent}%] 컨텍스트 ${percent}% 사용. 마일스톤이면 /compact를 권장합니다.`;
416
+ mergedOutput = mergeOutputs(mergedOutput, JSON.stringify({ systemMessage: msg }));
417
+ if (level === "warn") {
418
+ writeFileSync(nudgeMarker, new Date().toISOString());
419
+ }
420
+ }
421
+ }
422
+ } catch { /* 컨텍스트 모니터 읽기 실패 무시 */ }
423
+ }
424
+
343
425
  // ── PostToolUse:Skill 완료 시 라우팅 가중치 기록 ──
344
426
  if (eventName === "PostToolUse" && toolName === "Skill" && !blocked) {
345
427
  try {
346
428
  const input = JSON.parse(stdinRaw);
347
429
  const skillName = input.tool_input?.skill || "";
348
- if (skillName && skillName.startsWith("tfx-")) {
430
+ if (skillName?.startsWith("tfx-")) {
349
431
  const mode = skillName.replace(/^tfx-/, "");
350
432
  const gitRoot = process.env.GIT_WORK_TREE || process.cwd();
351
433
  const slug = gitRoot.split(/[\\/]/).pop() || "unknown";
352
434
  recordRouteOutcome(slug, mode, "completion");
353
435
  }
354
- } catch { /* 가중치 기록 실패 무시 */ }
436
+ } catch {
437
+ /* 가중치 기록 실패 무시 */
438
+ }
355
439
  }
356
440
 
357
441
  // 결과 출력
@@ -192,6 +192,17 @@
192
192
  "blocking": false,
193
193
  "description": "supergateway MCP 서비스 헬스체크 및 자동 기동"
194
194
  },
195
+ {
196
+ "id": "tfx-session-stale-cleanup",
197
+ "source": "triflux",
198
+ "matcher": "*",
199
+ "command": "node \"${PLUGIN_ROOT}/scripts/session-stale-cleanup.mjs\"",
200
+ "priority": 5,
201
+ "enabled": true,
202
+ "timeout": 3,
203
+ "blocking": false,
204
+ "description": "이전 세션의 stale tfx-multi 상태 파일 정리 (#62)"
205
+ },
195
206
  {
196
207
  "id": "ext-session-vault-start",
197
208
  "source": "session-vault",
@@ -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
@@ -80,7 +73,10 @@
80
73
  { "source": "(?:분석해|계획|설계해)", "flags": "i" },
81
74
  { "source": "(?:찾아봐|조사해|검색해)", "flags": "i" },
82
75
  { "source": "(?:정리해|슬롭|클린업)", "flags": "i" },
83
- { "source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b", "flags": "i" }
76
+ {
77
+ "source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b",
78
+ "flags": "i"
79
+ }
84
80
  ],
85
81
  "skill": "tfx-auto",
86
82
  "priority": 2,
@@ -51,7 +51,7 @@ export function resolvePluginRoot(callerUrl) {
51
51
 
52
52
  const moduleFallback = toPluginRootFromUrl(import.meta.url) || process.cwd();
53
53
  process.stderr.write(
54
- `[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n`
54
+ `[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n`,
55
55
  );
56
56
  return moduleFallback;
57
57
  }
@@ -25,10 +25,14 @@ function buildSystemMessage(filePath, stdioServers, result) {
25
25
 
26
26
  if (result.modified) {
27
27
  const actionLabel = result.replacement ? "자동 치환" : "자동 제거";
28
- lines.push(`[mcp-guard] stdio MCP ${actionLabel}: ${stdioServers.map((server) => server.name).join(", ")}`);
28
+ lines.push(
29
+ `[mcp-guard] stdio MCP ${actionLabel}: ${stdioServers.map((server) => server.name).join(", ")}`,
30
+ );
29
31
 
30
32
  if (result.replacement?.name && result.replacement?.url) {
31
- lines.push(`[mcp-guard] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`);
33
+ lines.push(
34
+ `[mcp-guard] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`,
35
+ );
32
36
  }
33
37
 
34
38
  if (result.backupPath) {
@@ -11,11 +11,8 @@ let getPipelineStateDbPath;
11
11
  let ensurePipelineTable;
12
12
  let listPipelineStates;
13
13
  try {
14
- ({
15
- getPipelineStateDbPath,
16
- ensurePipelineTable,
17
- listPipelineStates,
18
- } = await import("../hub/pipeline/state.mjs"));
14
+ ({ getPipelineStateDbPath, ensurePipelineTable, listPipelineStates } =
15
+ await import("../hub/pipeline/state.mjs"));
19
16
  } catch {
20
17
  // hub/pipeline 모듈 없으면 훅 무동작
21
18
  process.exit(0);
@@ -53,7 +50,7 @@ try {
53
50
  // 활성 파이프라인 발견 → 구조화 decision으로 block
54
51
  const lines = active.map(
55
52
  (s) =>
56
- ` - 팀 ${s.team_name}: ${s.phase} 단계 (fix: ${s.fix_attempt}/${s.fix_max}, ralph: ${s.ralph_iteration}/${s.ralph_max})`
53
+ ` - 팀 ${s.team_name}: ${s.phase} 단계 (fix: ${s.fix_attempt}/${s.fix_max}, ralph: ${s.ralph_iteration}/${s.ralph_max})`,
57
54
  );
58
55
 
59
56
  const reason =
@@ -8,23 +8,51 @@
8
8
  // BLOCK (exit 2) — 복구 불가능한 파괴적 명령
9
9
  // WARN (allow + context) — 주의가 필요한 명령
10
10
 
11
- import { readFileSync, existsSync } from "node:fs";
11
+ import { existsSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
 
14
14
  // ── 차단 규칙 ──────────────────────────────────────────────
15
15
  const BLOCK_RULES = [
16
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
17
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i, reason: "현재 디렉토리 rm -rf . 차단" },
18
- { pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i, reason: "main/master force push 차단" },
19
- { pattern: /\bgit\s+push\s+--force\s*$/i, reason: "대상 미지정 force push 차단" },
20
- { pattern: /\bgit\s+reset\s+--hard\s+origin\//i, reason: "remote reset --hard 차단 — 로컬 작업 소실 위험" },
16
+ {
17
+ pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i,
18
+ reason: "루트/홈 디렉토리 rm -rf 차단",
19
+ },
20
+ {
21
+ pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i,
22
+ reason: "현재 디렉토리 rm -rf . 차단",
23
+ },
24
+ {
25
+ pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i,
26
+ reason: "main/master force push 차단",
27
+ },
28
+ {
29
+ pattern: /\bgit\s+push\s+--force\s*$/i,
30
+ reason: "대상 미지정 force push 차단",
31
+ },
32
+ {
33
+ pattern: /\bgit\s+reset\s+--hard\s+origin\//i,
34
+ reason: "remote reset --hard 차단 — 로컬 작업 소실 위험",
35
+ },
21
36
  { pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "SQL DROP 차단" },
22
37
  { pattern: /\btruncate\s+table\b/i, reason: "SQL TRUNCATE 차단" },
23
38
  { pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
24
39
  { pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
25
- { pattern: /\bgit\s+clean\s+.*-fd/i, reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험" },
26
- { pattern: /\bpsmux\s+kill-session\b/i, reason: "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>", skipIfGit: true },
27
- { pattern: /\bpsmux\s+kill-server\b/i, reason: "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용", skipIfGit: true },
40
+ {
41
+ pattern: /\bgit\s+clean\s+.*-fd/i,
42
+ reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험",
43
+ },
44
+ {
45
+ pattern: /\bpsmux\s+kill-session\b/i,
46
+ reason:
47
+ "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>",
48
+ skipIfGit: true,
49
+ },
50
+ {
51
+ pattern: /\bpsmux\s+kill-server\b/i,
52
+ reason:
53
+ "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용",
54
+ skipIfGit: true,
55
+ },
28
56
  ];
29
57
 
30
58
  const WT_DIRECT_PATTERNS = [
@@ -42,12 +70,12 @@ const WT_DIRECT_BLOCK_MESSAGE =
42
70
  // ── SSH+PowerShell bash 문법 차단 ────────────────────────────
43
71
  // 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
44
72
  const BASH_SYNTAX_IN_SSH = [
45
- /2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
46
- />\s*\/dev\/null/, // >/dev/null
47
- /&>\s*\/dev\/null/, // &>/dev/null
48
- /\$\(/, // $(cmd) → PowerShell에서 다른 의미
49
- /\bsource\s+/, // source → PowerShell에 없음
50
- /\bexport\s+\w+=/, // export VAR= → PowerShell에 없음
73
+ /2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
74
+ />\s*\/dev\/null/, // >/dev/null
75
+ /&>\s*\/dev\/null/, // &>/dev/null
76
+ /\$\(/, // $(cmd) → PowerShell에서 다른 의미
77
+ /\bsource\s+/, // source → PowerShell에 없음
78
+ /\bexport\s+\w+=/, // export VAR= → PowerShell에 없음
51
79
  ];
52
80
 
53
81
  const SSH_POWERSHELL_HINT =
@@ -56,28 +84,59 @@ const SSH_POWERSHELL_HINT =
56
84
 
57
85
  // ── 경고 규칙 ──────────────────────────────────────────────
58
86
  const WARN_RULES = [
59
- { pattern: /\bgit\s+push\b(?!.*--force)/i, warn: "git push 감지. 원격 저장소에 반영됩니다." },
60
- { pattern: /\bgit\s+rebase\b/i, warn: "git rebase 감지. 커밋 히스토리가 변경됩니다." },
87
+ {
88
+ pattern: /\bgit\s+push\b(?!.*--force)/i,
89
+ warn: "git push 감지. 원격 저장소에 반영됩니다.",
90
+ },
91
+ {
92
+ pattern: /\bgit\s+rebase\b/i,
93
+ warn: "git rebase 감지. 커밋 히스토리가 변경됩니다.",
94
+ },
61
95
  { pattern: /\bgit\s+branch\s+-[dD]\b/i, warn: "브랜치 삭제 감지." },
62
- { pattern: /\bnpm\s+publish\b/i, warn: "npm publish 감지. 공개 레지스트리에 배포됩니다." },
63
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s/i, warn: "재귀 삭제 감지. 대상을 확인하세요." },
64
- { pattern: /--no-verify\b/i, warn: "--no-verify 감지. 건너뛰기는 권장하지 않습니다." },
96
+ {
97
+ pattern: /\bnpm\s+publish\b/i,
98
+ warn: "npm publish 감지. 공개 레지스트리에 배포됩니다.",
99
+ },
100
+ {
101
+ pattern: /\brm\s+(-[^\s]*)?-rf?\s/i,
102
+ warn: "재귀 삭제 감지. 대상을 확인하세요.",
103
+ },
104
+ {
105
+ pattern: /--no-verify\b/i,
106
+ warn: "--no-verify 감지. 훅 건너뛰기는 권장하지 않습니다.",
107
+ },
65
108
  { pattern: /\bchmod\s+777\b/i, warn: "chmod 777 감지. 보안 위험." },
66
- { pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i, warn: "curl | sh 감지. 원격 스크립트 실행 주의." },
109
+ {
110
+ pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i,
111
+ warn: "curl | sh 감지. 원격 스크립트 실행 주의.",
112
+ },
67
113
  ];
68
114
 
69
115
  // ── reflexion 적응형 패널티 로드 ──────────────────────────────
70
116
  function loadReflexionPenalties() {
71
117
  try {
72
118
  const home = process.env.HOME || process.env.USERPROFILE || "";
73
- const penaltyFile = join(home, ".triflux", "reflexion", "pending-penalties.jsonl");
119
+ const penaltyFile = join(
120
+ home,
121
+ ".triflux",
122
+ "reflexion",
123
+ "pending-penalties.jsonl",
124
+ );
74
125
  if (!existsSync(penaltyFile)) return [];
75
126
  return readFileSync(penaltyFile, "utf8")
76
127
  .split("\n")
77
128
  .filter(Boolean)
78
- .map(line => { try { return JSON.parse(line); } catch { return null; } })
129
+ .map((line) => {
130
+ try {
131
+ return JSON.parse(line);
132
+ } catch {
133
+ return null;
134
+ }
135
+ })
79
136
  .filter(Boolean);
80
- } catch { return []; }
137
+ } catch {
138
+ return [];
139
+ }
81
140
  }
82
141
 
83
142
  function readStdin() {
@@ -89,7 +148,11 @@ function readStdin() {
89
148
  }
90
149
 
91
150
  function shouldSkipSegment(segment) {
92
- return !segment || segment.startsWith("#") || /^\s*(echo|printf|grep|git\s+commit)\b/i.test(segment);
151
+ return (
152
+ !segment ||
153
+ segment.startsWith("#") ||
154
+ /^\s*(echo|printf|grep|git\s+commit)\b/i.test(segment)
155
+ );
93
156
  }
94
157
 
95
158
  function hasSegmentInvocation(cmd, patterns) {
@@ -122,7 +185,7 @@ function blockCommand(message, command) {
122
185
  process.stderr.write(
123
186
  `${message}\n` +
124
187
  `명령어: ${command.slice(0, 120)}${command.length > 120 ? "..." : ""}\n` +
125
- "이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요."
188
+ "이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요.",
126
189
  );
127
190
  process.exit(2);
128
191
  }
@@ -161,7 +224,15 @@ function main() {
161
224
  const penalties = loadReflexionPenalties();
162
225
  if (penalties.length > 0) {
163
226
  for (const penalty of penalties) {
164
- if (penalty.error_pattern && new RegExp(penalty.error_pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 80), "i").test(command)) {
227
+ if (
228
+ penalty.error_pattern &&
229
+ new RegExp(
230
+ penalty.error_pattern
231
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
232
+ .slice(0, 80),
233
+ "i",
234
+ ).test(command)
235
+ ) {
165
236
  const output = {
166
237
  hookSpecificOutput: {
167
238
  hookEventName: "PreToolUse",
@@ -186,7 +257,7 @@ function main() {
186
257
  const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
187
258
  if (!sshMatch) continue;
188
259
  const sshPayload = sshMatch[1];
189
- const bashSyntax = BASH_SYNTAX_IN_SSH.find(p => p.test(sshPayload));
260
+ const bashSyntax = BASH_SYNTAX_IN_SSH.find((p) => p.test(sshPayload));
190
261
  if (bashSyntax) {
191
262
  blockCommand(
192
263
  `[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
@@ -0,0 +1,143 @@
1
+ // hooks/session-start-fast.mjs — SessionStart in-process fast-path
2
+ //
3
+ // hook-orchestrator.mjs가 SessionStart 이벤트일 때 이 모듈을 dynamic import.
4
+ // 6개 훅을 1개 node 프로세스 안에서 실행하여 콜드스타트 7회 → 1회로 줄인다.
5
+ //
6
+ // 분류:
7
+ // BLOCKING (직렬, stdout 반환 전 완료): setup.runCritical, mcp-safety-guard.run
8
+ // DEFERRED (병렬, 실패해도 안 죽음): hub-ensure.run, mcp-gateway-ensure.run, setup.runDeferred
9
+ // BACKGROUND (fire-and-forget): preflight-cache.run
10
+ //
11
+ // external source 훅 (session-vault 등)은 여전히 execFile로 실행된다.
12
+
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { createModuleLogger } from "../scripts/lib/logger.mjs";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const SCRIPTS = join(__dirname, "..", "scripts");
19
+
20
+ const log = createModuleLogger("session-start-fast");
21
+
22
+ /**
23
+ * BLOCKING 훅을 순차 실행. 하나라도 throw하면 로그만 남기고 계속.
24
+ * @param {string} stdinData
25
+ * @returns {Promise<{stdout: string, stderr: string}>}
26
+ */
27
+ async function runBlocking(stdinData) {
28
+ const output = { stdout: "", stderr: "" };
29
+ const timings = [];
30
+
31
+ // 1. setup.runCritical — 환경 초기화 필수
32
+ try {
33
+ const t0 = performance.now();
34
+ const setup = await import(join(SCRIPTS, "setup.mjs"));
35
+ const result = await setup.runCritical(stdinData);
36
+ const dur = performance.now() - t0;
37
+ timings.push({ hook: "setup.critical", dur_ms: Math.round(dur) });
38
+ if (result?.stdout) output.stdout += result.stdout + "\n";
39
+ if (result?.stderr) output.stderr += result.stderr + "\n";
40
+ log.info({ hook: "setup.critical", dur_ms: Math.round(dur) }, "hook.completed");
41
+ } catch (err) {
42
+ log.error({ hook: "setup.critical", err: String(err.message || err) }, "hook.failed");
43
+ }
44
+
45
+ // 2. mcp-safety-guard.run — EPERM 방지
46
+ try {
47
+ const t0 = performance.now();
48
+ const guard = await import(join(SCRIPTS, "mcp-safety-guard.mjs"));
49
+ guard.run();
50
+ const dur = performance.now() - t0;
51
+ timings.push({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) });
52
+ log.info({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) }, "hook.completed");
53
+ } catch (err) {
54
+ log.error({ hook: "mcp-safety-guard", err: String(err.message || err) }, "hook.failed");
55
+ }
56
+
57
+ return { ...output, timings };
58
+ }
59
+
60
+ /**
61
+ * DEFERRED 훅을 병렬 실행. 실패해도 crash 안 함, 로그만 남김.
62
+ * Promise는 의도적으로 관리하지 않음 (fire-and-forget with logging).
63
+ * @param {string} stdinData
64
+ */
65
+ function runDeferred(stdinData) {
66
+ const tasks = [
67
+ {
68
+ name: "hub-ensure",
69
+ fn: async () => {
70
+ const mod = await import(join(SCRIPTS, "hub-ensure.mjs"));
71
+ return mod.run(stdinData);
72
+ },
73
+ },
74
+ {
75
+ name: "mcp-gateway-ensure",
76
+ fn: async () => {
77
+ const mod = await import(join(SCRIPTS, "mcp-gateway-ensure.mjs"));
78
+ return mod.run(stdinData);
79
+ },
80
+ },
81
+ {
82
+ name: "setup.deferred",
83
+ fn: async () => {
84
+ const mod = await import(join(SCRIPTS, "setup.mjs"));
85
+ return mod.runDeferred(stdinData);
86
+ },
87
+ },
88
+ ];
89
+
90
+ for (const task of tasks) {
91
+ const t0 = performance.now();
92
+ task.fn()
93
+ .then((result) => {
94
+ const dur = performance.now() - t0;
95
+ log.info({ hook: task.name, dur_ms: Math.round(dur), code: result?.code }, "deferred.completed");
96
+ })
97
+ .catch((err) => {
98
+ const dur = performance.now() - t0;
99
+ log.error({ hook: task.name, dur_ms: Math.round(dur), err: String(err.message || err) }, "deferred.failed");
100
+ });
101
+ }
102
+ }
103
+
104
+ /**
105
+ * BACKGROUND 훅. fire-and-forget.
106
+ * @param {string} stdinData
107
+ */
108
+ function runBackground(stdinData) {
109
+ // preflight-cache
110
+ import(join(SCRIPTS, "preflight-cache.mjs"))
111
+ .then((mod) => mod.run(stdinData))
112
+ .catch(() => {}); // 완전 무시
113
+
114
+ // session-vault은 external source — hook-orchestrator가 execFile로 실행
115
+ }
116
+
117
+ /**
118
+ * SessionStart fast-path 진입점.
119
+ * hook-orchestrator.mjs에서 호출된다.
120
+ *
121
+ * @param {string} stdinData — orchestrator가 전달하는 stdin JSON
122
+ * @param {Array} externalHooks — source !== 'triflux'인 훅 목록 (orchestrator가 execFile로 실행)
123
+ * @returns {Promise<{stdout: string, stderr: string, timings: Array}>}
124
+ */
125
+ export async function execute(stdinData, externalHooks = []) {
126
+ const totalStart = performance.now();
127
+
128
+ // BLOCKING: 프롬프트 전 완료 필수
129
+ const blocking = await runBlocking(stdinData);
130
+
131
+ // DEFERRED + BACKGROUND: fire-and-forget
132
+ runDeferred(stdinData);
133
+ runBackground(stdinData);
134
+
135
+ const totalDur = performance.now() - totalStart;
136
+ log.info({ total_ms: Math.round(totalDur), blocking_count: 2, deferred_count: 3, bg_count: 1 }, "session-start.done");
137
+
138
+ return {
139
+ stdout: blocking.stdout,
140
+ stderr: blocking.stderr,
141
+ timings: blocking.timings,
142
+ };
143
+ }