@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.
- package/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +47 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +279 -33
- package/hooks/hook-registry.json +104 -7
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- 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 +308 -18
- 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 +832 -291
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -3
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -17
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +171 -39
- 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 +303 -107
- 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 +116 -58
- package/hub/research.mjs +34 -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 +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +129 -63
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +76 -72
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -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 +184 -51
- package/hud/renderers.mjs +413 -123
- 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 +67 -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 +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +393 -249
- 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 +1282 -116
- 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 +72 -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 +36 -11
- 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,42 +466,131 @@ 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
|
+
}
|
|
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
|
}
|
package/hooks/hook-registry.json
CHANGED
|
@@ -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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
}
|