@triflux/core 10.1.0 → 10.26.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.
- package/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +191 -31
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +262 -34
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +1051 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +82 -108
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +210 -52
- package/hud/renderers.mjs +440 -127
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +49 -20
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +117 -12
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- 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 {
|
|
23
|
-
import {
|
|
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)
|
|
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) {
|
|
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(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 (
|
|
256
|
+
if (
|
|
257
|
+
accumulated.hookSpecificOutput?.additionalContext &&
|
|
258
|
+
parsed.hookSpecificOutput.additionalContext
|
|
259
|
+
) {
|
|
174
260
|
parsed.hookSpecificOutput.additionalContext =
|
|
175
|
-
accumulated.hookSpecificOutput.additionalContext +
|
|
261
|
+
accumulated.hookSpecificOutput.additionalContext +
|
|
262
|
+
"\n" +
|
|
263
|
+
parsed.hookSpecificOutput.additionalContext;
|
|
176
264
|
}
|
|
177
|
-
accumulated.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
|
-
: {
|
|
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,34 +466,92 @@ async function main() {
|
|
|
309
466
|
|
|
310
467
|
let mergedOutput = null;
|
|
311
468
|
let blocked = false;
|
|
469
|
+
let blockingStderr = "";
|
|
312
470
|
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
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
|
+
}
|
|
339
552
|
}
|
|
553
|
+
} catch {
|
|
554
|
+
/* 컨텍스트 모니터 읽기 실패 무시 */
|
|
340
555
|
}
|
|
341
556
|
}
|
|
342
557
|
|
|
@@ -345,20 +560,37 @@ async function main() {
|
|
|
345
560
|
try {
|
|
346
561
|
const input = JSON.parse(stdinRaw);
|
|
347
562
|
const skillName = input.tool_input?.skill || "";
|
|
348
|
-
if (skillName
|
|
563
|
+
if (skillName?.startsWith("tfx-")) {
|
|
349
564
|
const mode = skillName.replace(/^tfx-/, "");
|
|
350
565
|
const gitRoot = process.env.GIT_WORK_TREE || process.cwd();
|
|
351
566
|
const slug = gitRoot.split(/[\\/]/).pop() || "unknown";
|
|
352
567
|
recordRouteOutcome(slug, mode, "completion");
|
|
353
568
|
}
|
|
354
|
-
} catch {
|
|
569
|
+
} catch {
|
|
570
|
+
/* 가중치 기록 실패 무시 */
|
|
571
|
+
}
|
|
355
572
|
}
|
|
356
573
|
|
|
357
574
|
// 결과 출력
|
|
358
575
|
if (blocked) {
|
|
576
|
+
if (dedupePreToolUseBash) {
|
|
577
|
+
writeHookCache(stdinRaw, {
|
|
578
|
+
blocked: true,
|
|
579
|
+
mergedOutput: null,
|
|
580
|
+
stderr: blockingStderr,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
359
583
|
process.exit(2);
|
|
360
584
|
}
|
|
361
585
|
|
|
586
|
+
if (dedupePreToolUseBash) {
|
|
587
|
+
writeHookCache(stdinRaw, {
|
|
588
|
+
blocked: false,
|
|
589
|
+
mergedOutput,
|
|
590
|
+
stderr: "",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
362
594
|
if (mergedOutput) {
|
|
363
595
|
process.stdout.write(JSON.stringify(mergedOutput));
|
|
364
596
|
}
|