@triflux/core 10.0.3 → 10.25.1

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 (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +47 -7
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +279 -33
  7. package/hooks/hook-registry.json +104 -7
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +308 -18
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +832 -291
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -3
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -17
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +171 -39
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +303 -107
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +116 -58
  75. package/hub/research.mjs +34 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +129 -63
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +76 -72
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +393 -249
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1282 -116
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +72 -12
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +36 -11
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. package/scripts/lib/toml.mjs +113 -0
@@ -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,15 +20,30 @@
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 { createHash } from "node:crypto";
25
+ import {
26
+ existsSync,
27
+ mkdirSync,
28
+ readFileSync,
29
+ unlinkSync,
30
+ writeFileSync,
31
+ } from "node:fs";
32
+ import { tmpdir } from "node:os";
33
+ import { dirname, join } from "node:path";
24
34
  import { fileURLToPath } from "node:url";
25
- import { execFileSync, execFile } from "node:child_process";
26
35
  import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
27
36
 
28
37
  const __dirname = dirname(fileURLToPath(import.meta.url));
29
38
  const REGISTRY_PATH =
30
39
  process.env.TRIFLUX_HOOK_REGISTRY || join(__dirname, "hook-registry.json");
40
+ const HOOK_CACHE_DIR =
41
+ process.env.TRIFLUX_HOOK_CACHE_DIR ||
42
+ join(tmpdir(), "triflux-hook-orchestrator-cache");
43
+ const HOOK_CACHE_TTL_MS = Math.max(
44
+ 0,
45
+ Number.parseInt(process.env.TRIFLUX_HOOK_CACHE_TTL_MS || "1500", 10) || 1500,
46
+ );
31
47
 
32
48
  // ── stdin 읽기 ──────────────────────────────────────────────
33
49
  function readStdin() {
@@ -75,6 +91,57 @@ function matchesMatcher(hookMatcher, toolName, eventInput) {
75
91
  });
76
92
  }
77
93
 
94
+ function shouldDedupePreToolUseBash(eventName, toolName) {
95
+ return eventName === "PreToolUse" && toolName === "Bash";
96
+ }
97
+
98
+ function buildHookCachePath(stdinData) {
99
+ const digest = createHash("sha1").update(stdinData).digest("hex");
100
+ return join(HOOK_CACHE_DIR, `pretool-bash-${digest}.json`);
101
+ }
102
+
103
+ function readHookCache(stdinData) {
104
+ if (HOOK_CACHE_TTL_MS <= 0) return null;
105
+
106
+ const cachePath = buildHookCachePath(stdinData);
107
+ if (!existsSync(cachePath)) return null;
108
+
109
+ try {
110
+ const cached = JSON.parse(readFileSync(cachePath, "utf8"));
111
+ const ageMs = Date.now() - Number(cached?.ts || 0);
112
+ if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > HOOK_CACHE_TTL_MS) {
113
+ unlinkSync(cachePath);
114
+ return null;
115
+ }
116
+ return cached;
117
+ } catch {
118
+ try {
119
+ unlinkSync(cachePath);
120
+ } catch {}
121
+ return null;
122
+ }
123
+ }
124
+
125
+ function writeHookCache(stdinData, payload) {
126
+ if (HOOK_CACHE_TTL_MS <= 0) return;
127
+
128
+ try {
129
+ mkdirSync(HOOK_CACHE_DIR, { recursive: true });
130
+ writeFileSync(
131
+ buildHookCachePath(stdinData),
132
+ JSON.stringify({ ts: Date.now(), ...payload }),
133
+ "utf8",
134
+ );
135
+ } catch {
136
+ // 캐시 실패는 훅 실행에 영향 주지 않음
137
+ }
138
+ }
139
+
140
+ function shouldShortCircuitPreToolUseBash(result) {
141
+ if (!result || result.code === 2) return true;
142
+ return result.code === 0 && Boolean(result.stdout?.trim());
143
+ }
144
+
78
145
  // ── 단일 훅 실행 ────────────────────────────────────────────
79
146
  function executeHook(hook, stdinData) {
80
147
  const cmd = resolveCommand(hook.command);
@@ -84,7 +151,8 @@ function executeHook(hook, stdinData) {
84
151
  // "bash script.sh" → ["bash", ["script.sh"]]
85
152
  // 따옴표 처리 포함
86
153
  const parts = parseCommand(cmd);
87
- if (parts.length === 0) return { code: 1, stdout: "", stderr: "empty command" };
154
+ if (parts.length === 0)
155
+ return { code: 1, stdout: "", stderr: "empty command" };
88
156
 
89
157
  const [executable, ...args] = parts;
90
158
 
@@ -114,20 +182,35 @@ function executeHookAsync(hook, stdinData) {
114
182
  const cmd = resolveCommand(hook.command);
115
183
  const timeout = (hook.timeout || 10) * 1000;
116
184
  const parts = parseCommand(cmd);
117
- if (parts.length === 0) { resolve({ code: 1, stdout: "", stderr: "empty command" }); return; }
185
+ if (parts.length === 0) {
186
+ resolve({ code: 1, stdout: "", stderr: "empty command" });
187
+ return;
188
+ }
118
189
  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();
190
+ const child = execFile(
191
+ executable,
192
+ args,
193
+ {
194
+ timeout,
195
+ encoding: "utf8",
196
+ windowsHide: true,
197
+ cwd: process.cwd(),
198
+ env: { ...process.env },
199
+ },
200
+ (err, stdout, stderr) => {
201
+ if (err)
202
+ resolve({
203
+ code: err.status ?? 1,
204
+ stdout: stdout || "",
205
+ stderr: stderr || "",
206
+ });
207
+ else resolve({ code: 0, stdout: stdout || "", stderr: "" });
208
+ },
209
+ );
210
+ if (stdinData) {
211
+ child.stdin.write(stdinData);
212
+ child.stdin.end();
213
+ } else child.stdin.end();
131
214
  });
132
215
  }
133
216
 
@@ -170,11 +253,19 @@ function mergeOutputs(accumulated, newOutput) {
170
253
 
171
254
  // hookSpecificOutput 머지 — additionalContext는 누적, 나머지는 덮어쓰기
172
255
  if (parsed.hookSpecificOutput) {
173
- if (accumulated.hookSpecificOutput?.additionalContext && parsed.hookSpecificOutput.additionalContext) {
256
+ if (
257
+ accumulated.hookSpecificOutput?.additionalContext &&
258
+ parsed.hookSpecificOutput.additionalContext
259
+ ) {
174
260
  parsed.hookSpecificOutput.additionalContext =
175
- accumulated.hookSpecificOutput.additionalContext + "\n" + parsed.hookSpecificOutput.additionalContext;
261
+ accumulated.hookSpecificOutput.additionalContext +
262
+ "\n" +
263
+ parsed.hookSpecificOutput.additionalContext;
176
264
  }
177
- accumulated.hookSpecificOutput = { ...accumulated.hookSpecificOutput, ...parsed.hookSpecificOutput };
265
+ accumulated.hookSpecificOutput = {
266
+ ...accumulated.hookSpecificOutput,
267
+ ...parsed.hookSpecificOutput,
268
+ };
178
269
  }
179
270
  // systemMessage는 누적
180
271
  if (parsed.systemMessage) {
@@ -220,7 +311,12 @@ function recordRouteOutcome(slug, mode, outcome) {
220
311
 
221
312
  const weights = existsSync(weightsPath)
222
313
  ? JSON.parse(readFileSync(weightsPath, "utf8"))
223
- : { updated_at: null, total_routes: 0, overrides: 0, weights: { mode_bias: {}, profile_bias: {}, depth_bias: {} } };
314
+ : {
315
+ updated_at: null,
316
+ total_routes: 0,
317
+ overrides: 0,
318
+ weights: { mode_bias: {}, profile_bias: {}, depth_bias: {} },
319
+ };
224
320
 
225
321
  weights.total_routes++;
226
322
  weights.updated_at = new Date().toISOString();
@@ -247,6 +343,23 @@ function recordRouteOutcome(slug, mode, outcome) {
247
343
  writeFileSync(weightsPath, JSON.stringify(weights, null, 2), "utf8");
248
344
  }
249
345
 
346
+ function isHubTokenMonitorSnapshot(snapshot) {
347
+ if (!snapshot || typeof snapshot !== "object") return false;
348
+ return (
349
+ Object.hasOwn(snapshot, "requestTokens") ||
350
+ Object.hasOwn(snapshot, "responseTokens") ||
351
+ Object.hasOwn(snapshot, "totalUpdates") ||
352
+ (snapshot.byTool && typeof snapshot.byTool === "object")
353
+ );
354
+ }
355
+
356
+ function readCompactNudgePercent(snapshot) {
357
+ if (isHubTokenMonitorSnapshot(snapshot)) return null;
358
+ const percent = Number(snapshot?.percent ?? 0);
359
+ if (!Number.isFinite(percent)) return null;
360
+ return Math.max(0, Math.min(100, percent));
361
+ }
362
+
250
363
  // ── 메인 ────────────────────────────────────────────────────
251
364
  async function main() {
252
365
  // CLI: --record-route <slug> <mode> <outcome>
@@ -284,6 +397,50 @@ async function main() {
284
397
 
285
398
  if (!eventName) process.exit(0);
286
399
 
400
+ const dedupePreToolUseBash = shouldDedupePreToolUseBash(eventName, toolName);
401
+ if (dedupePreToolUseBash) {
402
+ const cached = readHookCache(stdinRaw);
403
+ if (cached?.stderr) process.stderr.write(cached.stderr);
404
+ if (cached?.blocked) process.exit(2);
405
+ if (cached?.mergedOutput) {
406
+ process.stdout.write(JSON.stringify(cached.mergedOutput));
407
+ }
408
+ if (cached) process.exit(0);
409
+ }
410
+
411
+ // ── SessionStart fast-path ──
412
+ // TRIFLUX_HOOK_FAST_PATH=false로 비활성화 가능 (rollback)
413
+ if (
414
+ eventName === "SessionStart" &&
415
+ process.env.TRIFLUX_HOOK_FAST_PATH !== "false"
416
+ ) {
417
+ try {
418
+ const { execute } = await import("./session-start-fast.mjs");
419
+ const result = await execute(stdinRaw);
420
+ if (result.stdout?.trim()) {
421
+ const output = { additionalContext: result.stdout.trim() };
422
+ process.stdout.write(JSON.stringify(output));
423
+ }
424
+
425
+ // external source 훅 (session-vault 등)은 기존 방식으로 실행
426
+ const allHooks = registry.events.SessionStart || [];
427
+ const externalHooks = allHooks.filter(
428
+ (h) => h.enabled !== false && h.source !== "triflux",
429
+ );
430
+ for (const hook of externalHooks) {
431
+ const hookResult = executeHookAsync(hook, stdinRaw);
432
+ hookResult.catch(() => {}); // fire-and-forget for external hooks
433
+ }
434
+
435
+ process.exit(0);
436
+ } catch (err) {
437
+ // fast-path 실패 시 기존 방식으로 폴백
438
+ process.stderr.write(
439
+ `[orchestrator] fast-path failed, falling back: ${err.message}\n`,
440
+ );
441
+ }
442
+ }
443
+
287
444
  // 이벤트에 해당하는 훅 목록
288
445
  const hooks = registry.events[eventName];
289
446
  if (!hooks || hooks.length === 0) process.exit(0);
@@ -309,42 +466,131 @@ async function main() {
309
466
 
310
467
  let mergedOutput = null;
311
468
  let blocked = false;
469
+ let blockingStderr = "";
312
470
 
313
- for (const group of groups) {
314
- if (blocked) break;
315
-
316
- if (group.hooks.length === 1) {
317
- // 단일 훅 — 기존 동기 실행
318
- const result = executeHook(group.hooks[0], stdinRaw);
471
+ if (dedupePreToolUseBash) {
472
+ for (const hook of matched) {
473
+ const result = executeHook(hook, stdinRaw);
319
474
  if (result.code === 2) {
320
- if (result.stderr) process.stderr.write(result.stderr);
475
+ blockingStderr = result.stderr || "";
476
+ if (blockingStderr) process.stderr.write(blockingStderr);
321
477
  blocked = true;
322
478
  break;
323
479
  }
324
480
  if (result.code === 0 && result.stdout.trim()) {
325
481
  mergedOutput = mergeOutputs(mergedOutput, result.stdout.trim());
326
482
  }
327
- } else {
328
- // 같은 priority 다중 훅 — 비동기 병렬 실행
329
- const results = await Promise.all(group.hooks.map((h) => executeHookAsync(h, stdinRaw)));
330
- for (const result of results) {
483
+ if (shouldShortCircuitPreToolUseBash(result)) {
484
+ break;
485
+ }
486
+ }
487
+ } else {
488
+ for (const group of groups) {
489
+ if (blocked) break;
490
+
491
+ if (group.hooks.length === 1) {
492
+ // 단일 훅 — 기존 동기 실행
493
+ const result = executeHook(group.hooks[0], stdinRaw);
331
494
  if (result.code === 2) {
332
- if (result.stderr) process.stderr.write(result.stderr);
495
+ blockingStderr = result.stderr || "";
496
+ if (blockingStderr) process.stderr.write(blockingStderr);
333
497
  blocked = true;
334
498
  break;
335
499
  }
336
500
  if (result.code === 0 && result.stdout.trim()) {
337
501
  mergedOutput = mergeOutputs(mergedOutput, result.stdout.trim());
338
502
  }
503
+ } else {
504
+ // 같은 priority 다중 훅 — 비동기 병렬 실행
505
+ const results = await Promise.all(
506
+ group.hooks.map((h) => executeHookAsync(h, stdinRaw)),
507
+ );
508
+ for (const result of results) {
509
+ if (result.code === 2) {
510
+ blockingStderr = result.stderr || "";
511
+ if (blockingStderr) process.stderr.write(blockingStderr);
512
+ blocked = true;
513
+ break;
514
+ }
515
+ if (result.code === 0 && result.stdout.trim()) {
516
+ mergedOutput = mergeOutputs(mergedOutput, result.stdout.trim());
517
+ }
518
+ }
339
519
  }
340
520
  }
341
521
  }
342
522
 
523
+ // ── PostToolUse: 컨텍스트 압축 nudge (인라인, 프로세스 추가 없음) ──
524
+ if (eventName === "PostToolUse" && toolName !== "Bash" && !blocked) {
525
+ try {
526
+ const home = process.env.HOME || process.env.USERPROFILE || "";
527
+ const snapshotPath = join(
528
+ home,
529
+ ".claude",
530
+ "cache",
531
+ "tfx-hub",
532
+ "context-monitor.json",
533
+ );
534
+ const nudgeMarker = join(tmpdir(), "tfx-compact-nudge-sent");
535
+ if (existsSync(snapshotPath) && !existsSync(nudgeMarker)) {
536
+ const snap = JSON.parse(readFileSync(snapshotPath, "utf8"));
537
+ const percent = readCompactNudgePercent(snap);
538
+ if (percent != null && percent >= 80) {
539
+ const level = percent >= 90 ? "critical" : "warn";
540
+ const msg =
541
+ level === "critical"
542
+ ? `[context ${percent}%] 컨텍스트 ${percent}% 사용. /compact 또는 에이전트 분할을 강력 권장합니다.`
543
+ : `[context ${percent}%] 컨텍스트 ${percent}% 사용. 마일스톤이면 /compact를 권장합니다.`;
544
+ mergedOutput = mergeOutputs(
545
+ mergedOutput,
546
+ JSON.stringify({ systemMessage: msg }),
547
+ );
548
+ if (level === "warn") {
549
+ writeFileSync(nudgeMarker, new Date().toISOString());
550
+ }
551
+ }
552
+ }
553
+ } catch {
554
+ /* 컨텍스트 모니터 읽기 실패 무시 */
555
+ }
556
+ }
557
+
558
+ // ── PostToolUse:Skill 완료 시 라우팅 가중치 기록 ──
559
+ if (eventName === "PostToolUse" && toolName === "Skill" && !blocked) {
560
+ try {
561
+ const input = JSON.parse(stdinRaw);
562
+ const skillName = input.tool_input?.skill || "";
563
+ if (skillName?.startsWith("tfx-")) {
564
+ const mode = skillName.replace(/^tfx-/, "");
565
+ const gitRoot = process.env.GIT_WORK_TREE || process.cwd();
566
+ const slug = gitRoot.split(/[\\/]/).pop() || "unknown";
567
+ recordRouteOutcome(slug, mode, "completion");
568
+ }
569
+ } catch {
570
+ /* 가중치 기록 실패 무시 */
571
+ }
572
+ }
573
+
343
574
  // 결과 출력
344
575
  if (blocked) {
576
+ if (dedupePreToolUseBash) {
577
+ writeHookCache(stdinRaw, {
578
+ blocked: true,
579
+ mergedOutput: null,
580
+ stderr: blockingStderr,
581
+ });
582
+ }
345
583
  process.exit(2);
346
584
  }
347
585
 
586
+ if (dedupePreToolUseBash) {
587
+ writeHookCache(stdinRaw, {
588
+ blocked: false,
589
+ mergedOutput,
590
+ stderr: "",
591
+ });
592
+ }
593
+
348
594
  if (mergedOutput) {
349
595
  process.stdout.write(JSON.stringify(mergedOutput));
350
596
  }
@@ -8,6 +8,19 @@
8
8
  "external_priority": 100
9
9
  },
10
10
  "events": {
11
+ "PermissionRequest": [
12
+ {
13
+ "id": "tfx-permission-safe-allow",
14
+ "source": "triflux",
15
+ "matcher": "Bash",
16
+ "command": "node \"${PLUGIN_ROOT}/hooks/permission-safe-allow.mjs\"",
17
+ "priority": 0,
18
+ "enabled": true,
19
+ "timeout": 2,
20
+ "blocking": false,
21
+ "description": "안전한 repo-local Bash PermissionRequest 자동 허용 (#245)"
22
+ }
23
+ ],
11
24
  "PreToolUse": [
12
25
  {
13
26
  "id": "tfx-safety-guard",
@@ -63,6 +76,16 @@
63
76
  "timeout": 2,
64
77
  "blocking": false,
65
78
  "description": "tfx-multi 상태 추적 게이트"
79
+ },
80
+ {
81
+ "id": "tfx-write-edit-passthrough",
82
+ "source": "triflux",
83
+ "matcher": "Edit|Write",
84
+ "command": "exit 0",
85
+ "priority": 999,
86
+ "enabled": true,
87
+ "blocking": false,
88
+ "description": "Edit/Write passthrough"
66
89
  }
67
90
  ],
68
91
  "PostToolUse": [
@@ -87,6 +110,17 @@
87
110
  "timeout": 3,
88
111
  "blocking": false,
89
112
  "description": "감시 대상 MCP 설정 변경 감지 → stdio MCP 자동 치환"
113
+ },
114
+ {
115
+ "id": "tfx-post-tool-tips",
116
+ "source": "triflux",
117
+ "matcher": "Bash|Edit|Write",
118
+ "command": "node \"${PLUGIN_ROOT}/hooks/post-tool-tips.mjs\"",
119
+ "priority": 2,
120
+ "enabled": true,
121
+ "timeout": 2,
122
+ "blocking": false,
123
+ "description": "중요 경로 변경 시 opt-in rules reminder 주입 (#245)"
90
124
  }
91
125
  ],
92
126
  "PostToolUseFailure": [
@@ -157,8 +191,8 @@
157
191
  "priority": 2,
158
192
  "enabled": true,
159
193
  "timeout": 8,
160
- "blocking": false,
161
- "description": "tfx-hub 서비스 헬스체크 및 시작"
194
+ "blocking": true,
195
+ "description": "tfx-hub 서비스 헬스체크 및 시작 (BLOCKING — 세션 시작 전 Hub 준비 보장)"
162
196
  },
163
197
  {
164
198
  "id": "tfx-preflight-cache",
@@ -182,13 +216,25 @@
182
216
  "blocking": false,
183
217
  "description": "supergateway MCP 서비스 헬스체크 및 자동 기동"
184
218
  },
219
+ {
220
+ "id": "tfx-session-stale-cleanup",
221
+ "source": "triflux",
222
+ "matcher": "*",
223
+ "command": "node \"${PLUGIN_ROOT}/scripts/session-stale-cleanup.mjs\"",
224
+ "priority": 5,
225
+ "enabled": true,
226
+ "timeout": 3,
227
+ "blocking": false,
228
+ "description": "이전 세션의 stale tfx-multi 상태 파일 정리 (#62)"
229
+ },
185
230
  {
186
231
  "id": "ext-session-vault-start",
187
232
  "source": "session-vault",
233
+ "requires": "$HOME/Desktop/Projects/tools/session-vault",
188
234
  "matcher": "*",
189
235
  "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/start_hook.sh\"",
190
236
  "priority": 100,
191
- "enabled": true,
237
+ "enabled": false,
192
238
  "timeout": 10,
193
239
  "blocking": false,
194
240
  "description": "세션 볼트 로깅 시작"
@@ -204,15 +250,16 @@
204
250
  "enabled": true,
205
251
  "timeout": 5,
206
252
  "blocking": true,
207
- "description": "활성 파이프라인 감지 → 종료 차단"
253
+ "description": "컨텍스트 guard/retry cap + 활성 파이프라인 감지 → Stop 차단"
208
254
  },
209
255
  {
210
256
  "id": "ext-session-vault-export",
211
257
  "source": "session-vault",
258
+ "requires": "$HOME/Desktop/Projects/tools/session-vault",
212
259
  "matcher": "*",
213
260
  "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/export_hook.sh\"",
214
261
  "priority": 100,
215
- "enabled": true,
262
+ "enabled": false,
216
263
  "timeout": 30,
217
264
  "blocking": false,
218
265
  "description": "세션 트랜스크립트 내보내기"
@@ -223,24 +270,74 @@
223
270
  "matcher": "*",
224
271
  "command": "powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"${HOME}/.claude/scripts/mcp-cleanup.ps1\"",
225
272
  "priority": 100,
226
- "enabled": true,
273
+ "enabled": false,
227
274
  "timeout": 8,
228
275
  "blocking": false,
229
276
  "description": "MCP 고아 프로세스 정리"
230
277
  }
231
278
  ],
279
+ "SessionEnd": [
280
+ {
281
+ "id": "tfx-session-end-cleanup",
282
+ "source": "triflux",
283
+ "matcher": "*",
284
+ "command": "node \"${PLUGIN_ROOT}/hooks/session-end-cleanup.mjs\"",
285
+ "priority": 0,
286
+ "enabled": true,
287
+ "timeout": 3,
288
+ "blocking": false,
289
+ "description": "세션 종료 시 lifecycle 상태 보고 및 opt-in 정리 (#245)"
290
+ }
291
+ ],
292
+ "SubagentStart": [
293
+ {
294
+ "id": "tfx-subagent-tracker",
295
+ "source": "triflux",
296
+ "matcher": "*",
297
+ "command": "node \"${PLUGIN_ROOT}/hooks/subagent-tracker.mjs\"",
298
+ "priority": 0,
299
+ "enabled": true,
300
+ "timeout": 2,
301
+ "blocking": false,
302
+ "description": "서브에이전트 lifecycle 시작/종료 상태 추적 (#245)"
303
+ }
304
+ ],
232
305
  "SubagentStop": [
306
+ {
307
+ "id": "tfx-subagent-tracker",
308
+ "source": "triflux",
309
+ "matcher": "*",
310
+ "command": "node \"${PLUGIN_ROOT}/hooks/subagent-tracker.mjs\"",
311
+ "priority": 0,
312
+ "enabled": true,
313
+ "timeout": 2,
314
+ "blocking": false,
315
+ "description": "서브에이전트 lifecycle 시작/종료 상태 추적 (#245)"
316
+ },
233
317
  {
234
318
  "id": "tfx-subagent-verifier",
235
319
  "source": "triflux",
236
320
  "matcher": "*",
237
321
  "command": "node \"${PLUGIN_ROOT}/hooks/subagent-verifier.mjs\"",
238
- "priority": 0,
322
+ "priority": 1,
239
323
  "enabled": true,
240
324
  "timeout": 3,
241
325
  "blocking": false,
242
326
  "description": "서브에이전트 결과 품질 체크"
243
327
  }
328
+ ],
329
+ "PreCompact": [
330
+ {
331
+ "id": "tfx-pre-compact-snapshot",
332
+ "source": "triflux",
333
+ "matcher": "*",
334
+ "command": "node \"${PLUGIN_ROOT}/hooks/pre-compact-snapshot.mjs\"",
335
+ "priority": 0,
336
+ "enabled": true,
337
+ "timeout": 3,
338
+ "blocking": false,
339
+ "description": "compact 직전 repo-local 진행 상태 스냅샷 주입 (#245)"
340
+ }
244
341
  ]
245
342
  }
246
343
  }
package/hooks/hooks.json CHANGED
@@ -25,6 +25,18 @@
25
25
  ]
26
26
  }
27
27
  ],
28
+ "PermissionRequest": [
29
+ {
30
+ "matcher": "Bash",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
35
+ "timeout": 3
36
+ }
37
+ ]
38
+ }
39
+ ],
28
40
  "PreToolUse": [
29
41
  {
30
42
  "matcher": "Bash|Agent|Skill",
@@ -39,7 +51,7 @@
39
51
  ],
40
52
  "PostToolUse": [
41
53
  {
42
- "matcher": "Edit|Write",
54
+ "matcher": "Bash|Edit|Write",
43
55
  "hooks": [
44
56
  {
45
57
  "type": "command",
@@ -73,6 +85,30 @@
73
85
  ]
74
86
  }
75
87
  ],
88
+ "SessionEnd": [
89
+ {
90
+ "matcher": "*",
91
+ "hooks": [
92
+ {
93
+ "type": "command",
94
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
95
+ "timeout": 5
96
+ }
97
+ ]
98
+ }
99
+ ],
100
+ "SubagentStart": [
101
+ {
102
+ "matcher": "*",
103
+ "hooks": [
104
+ {
105
+ "type": "command",
106
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
107
+ "timeout": 5
108
+ }
109
+ ]
110
+ }
111
+ ],
76
112
  "SubagentStop": [
77
113
  {
78
114
  "matcher": "*",
@@ -84,6 +120,18 @@
84
120
  }
85
121
  ]
86
122
  }
123
+ ],
124
+ "PreCompact": [
125
+ {
126
+ "matcher": "*",
127
+ "hooks": [
128
+ {
129
+ "type": "command",
130
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/hook-orchestrator.mjs\"",
131
+ "timeout": 5
132
+ }
133
+ ]
134
+ }
87
135
  ]
88
136
  }
89
137
  }