@triflux/core 10.48.1 → 10.50.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 (46) hide show
  1. package/hooks/agy-session-hook.mjs +1 -1
  2. package/hooks/claude-cwd-projection-refresh.mjs +7 -79
  3. package/hooks/codex-session-hook.mjs +3 -8
  4. package/hooks/hook-orchestrator.mjs +8 -648
  5. package/hooks/lib/resolve-root.mjs +1 -1
  6. package/hub/bridge.mjs +290 -51
  7. package/hub/team/claude-daemon-control.mjs +54 -6
  8. package/hub/team/claude-transcript.mjs +240 -0
  9. package/hub/team/session-context.mjs +203 -0
  10. package/hud/constants.mjs +10 -0
  11. package/hud/hud-qos-status.mjs +23 -4
  12. package/hud/providers/gemini.mjs +4 -1
  13. package/hud/renderers.mjs +10 -6
  14. package/package.json +2 -2
  15. package/scripts/lib/doctor-env-checks.mjs +0 -85
  16. package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
  17. package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
  18. package/scripts/lib/mcp-guard-engine.mjs +44 -271
  19. package/scripts/lib/mcp-manifest.mjs +2 -2
  20. package/scripts/lib/session-presence.mjs +249 -0
  21. package/hooks/agent-route-guard.mjs +0 -113
  22. package/hooks/cross-review-tracker.mjs +0 -135
  23. package/hooks/cto-north-star-brief.mjs +0 -133
  24. package/hooks/error-context.mjs +0 -185
  25. package/hooks/hook-adaptive-collector.mjs +0 -88
  26. package/hooks/hook-manager.mjs +0 -426
  27. package/hooks/hook-registry.json +0 -367
  28. package/hooks/hooks.json +0 -149
  29. package/hooks/keyword-rules.json +0 -878
  30. package/hooks/mcp-config-watcher.mjs +0 -84
  31. package/hooks/permission-safe-allow.mjs +0 -267
  32. package/hooks/pipeline-stop.mjs +0 -435
  33. package/hooks/post-tool-tips.mjs +0 -306
  34. package/hooks/pre-compact-snapshot.mjs +0 -181
  35. package/hooks/safety-guard.mjs +0 -466
  36. package/hooks/session-end-cleanup.mjs +0 -360
  37. package/hooks/session-start-fast.mjs +0 -579
  38. package/hooks/session-start-lake.mjs +0 -81
  39. package/hooks/subagent-tracker.mjs +0 -281
  40. package/hooks/subagent-verifier.mjs +0 -85
  41. package/scripts/lib/cross-review-utils.mjs +0 -59
  42. package/scripts/lib/hook-utils.mjs +0 -16
  43. package/scripts/lib/keyword-rules.mjs +0 -219
  44. package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
  45. package/scripts/lib/mcp-gateway-servers.mjs +0 -70
  46. package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
@@ -1,466 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // hooks/safety-guard.mjs — PreToolUse:Bash 훅
4
- //
5
- // 위험한 Bash 명령을 사전 차단(exit 2)하거나 경고(additionalContext)한다.
6
- // hooks.json에서 `if: "Bash(*)"` 필터와 함께 사용.
7
- //
8
- // 차단 레벨:
9
- // BLOCK (exit 2) — 복구 불가능한 파괴적 명령
10
- // WARN (allow + context) — 주의가 필요한 명령
11
-
12
- import { spawnSync } from "node:child_process";
13
- import { existsSync, readFileSync } from "node:fs";
14
- import { join } from "node:path";
15
- import { readHosts } from "../hub/lib/hosts-compat.mjs";
16
-
17
- // ── 로컬 우회 플래그 ──────────────────────────────────────────
18
- // LOCAL ONLY — Issue #89 대안 API 구현 전까지 psmux kill 시리즈 우회용.
19
- // 활성 조건 (OR):
20
- // 1) env: TFX_CLEANUP_BYPASS=1
21
- // 2) 파일: .claude/cleanup-bypass 존재 (repo root)
22
- // 파일 방식은 Claude Code Bash 도구가 훅에 env 전달 못하는 경우에도 동작.
23
- // 정식 해결: hub/team/psmux.mjs 에 listSessions/killSessionByTitle/pruneStale 노출.
24
- const CLEANUP_BYPASS = (() => {
25
- if (process.env.TFX_CLEANUP_BYPASS === "1") return true;
26
- try {
27
- return existsSync(join(process.cwd(), ".claude", "cleanup-bypass"));
28
- } catch {
29
- return false;
30
- }
31
- })();
32
-
33
- // ── 차단 규칙 ──────────────────────────────────────────────
34
- const BLOCK_RULES = [
35
- {
36
- pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i,
37
- reason: "루트/홈 디렉토리 rm -rf 차단",
38
- },
39
- {
40
- pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i,
41
- reason: "현재 디렉토리 rm -rf . 차단",
42
- },
43
- {
44
- pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i,
45
- reason: "main/master force push 차단",
46
- },
47
- {
48
- pattern: /\bgit\s+push\s+--force\s*$/i,
49
- reason: "대상 미지정 force push 차단",
50
- },
51
- {
52
- pattern: /\bgit\s+reset\s+--hard\s+origin\//i,
53
- reason: "remote reset --hard 차단 — 로컬 작업 소실 위험",
54
- },
55
- { pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "SQL DROP 차단" },
56
- { pattern: /\btruncate\s+table\b/i, reason: "SQL TRUNCATE 차단" },
57
- { pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
58
- { pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
59
- {
60
- pattern: /\bgit\s+clean\s+.*-fd/i,
61
- reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험",
62
- },
63
- {
64
- pattern: /\bpsmux\s+kill-session\b/i,
65
- reason:
66
- "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 대안: listSessions()/killSessionByTitle()/pruneStale() 또는 node hub/team/psmux.mjs --internal kill-by-title <prefix|/regex/> (또는 TFX_CLEANUP_BYPASS=1/.claude/cleanup-bypass)",
67
- skipIfGit: true,
68
- cleanupBypass: true,
69
- },
70
- {
71
- pattern: /\bpsmux\s+kill-server\b/i,
72
- reason:
73
- "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용 (또는 TFX_CLEANUP_BYPASS=1)",
74
- skipIfGit: true,
75
- cleanupBypass: true,
76
- },
77
- ];
78
-
79
- const WT_DIRECT_PATTERNS = [
80
- /\bwt\.exe\b/i,
81
- /\bwt\s+new-tab\b/i,
82
- /\bwt\s+split-pane\b/i,
83
- /\bwt\s+-w\b/i,
84
- /\bStart-Process\s+wt/i,
85
- /\bStart-Process\s+['"]?wt\.exe/i,
86
- ];
87
-
88
- const WT_DIRECT_BLOCK_MESSAGE =
89
- "[safety-guard] wt.exe 직접 호출 차단됨.\n" +
90
- "→ hub/team/wt-manager.mjs의 createWtManager() 팩토리 사용:\n" +
91
- " wt.createTab({ title, command, profile, cwd }) — 새 탭\n" +
92
- " wt.splitPane({ direction: 'H'|'V', title, command }) — 패인 분할\n" +
93
- " wt.applySplitLayout([{ title, command, direction }]) — 다중 배치\n" +
94
- "사용법: node -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.createTab({ title: '제목', command: 'pwsh' }); })\"";
95
-
96
- const PSMUX_INTERNAL_WRAPPER_PATTERNS = [
97
- /node(?:\.exe)?\s+.*hub[\\/]+team[\\/]+psmux\.mjs\s+--internal\s+(?:list|kill-by-title|prune-stale)\b/i,
98
- ];
99
-
100
- // ── SSH+PowerShell bash 문법 차단 ────────────────────────────
101
- // 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
102
- // macOS/Linux 대상 SSH에는 bash 문법이 정상이므로 hosts.json OS를 확인한다.
103
- const BASH_SYNTAX_IN_SSH = [
104
- /2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
105
- />\s*\/dev\/null/, // >/dev/null
106
- /&>\s*\/dev\/null/, // &>/dev/null
107
- /\$\(/, // $(cmd) → PowerShell에서 다른 의미
108
- /\bsource\s+/, // source → PowerShell에 없음
109
- /\bexport\s+\w+=/, // export VAR= → PowerShell에 없음
110
- ];
111
-
112
- const SSH_POWERSHELL_HINT =
113
- "원격 셸이 PowerShell입니다. bash 문법 직접 전달 금지. scp + pwsh -File 패턴 사용. " +
114
- "2>/dev/null → 2>$null, $() → $(), export → $env:, source → . (dot-source)";
115
-
116
- /** hosts.json에서 Windows 호스트 식별자 집합을 구축한다. */
117
- function getWindowsHostIds() {
118
- const ids = new Set();
119
- try {
120
- const registry = readHosts(process.cwd());
121
- for (const [name, cfg] of Object.entries(registry.hosts || {})) {
122
- if (cfg.os !== "windows") continue;
123
- ids.add(name);
124
- if (cfg.tailscale?.ip) ids.add(cfg.tailscale.ip);
125
- if (cfg.tailscale?.dns) ids.add(cfg.tailscale.dns);
126
- if (cfg.ssh?.host) ids.add(cfg.ssh.host);
127
- if (cfg.ssh?.user) {
128
- ids.add(`${cfg.ssh.user}@${name}`);
129
- if (cfg.tailscale?.ip) ids.add(`${cfg.ssh.user}@${cfg.tailscale.ip}`);
130
- if (cfg.tailscale?.dns) ids.add(`${cfg.ssh.user}@${cfg.tailscale.dns}`);
131
- if (cfg.ssh?.host) ids.add(`${cfg.ssh.user}@${cfg.ssh.host}`);
132
- }
133
- }
134
- } catch {
135
- // hosts.json 로드 실패 시 빈 집합 → 차단 안 함 (POSIX 기본)
136
- }
137
- return ids;
138
- }
139
-
140
- /** SSH 명령의 대상이 Windows 호스트인지 판별한다. */
141
- function isSshTargetWindows(command) {
142
- const winIds = getWindowsHostIds();
143
- if (winIds.size === 0) return false; // Windows 호스트 없으면 POSIX 가정
144
- for (const id of winIds) {
145
- if (command.includes(id)) return true;
146
- }
147
- return false;
148
- }
149
-
150
- // ── 경고 규칙 ──────────────────────────────────────────────
151
- const WARN_RULES = [
152
- {
153
- pattern: /\bgit\s+push\b(?!.*--force)/i,
154
- warn: "git push 감지. 원격 저장소에 반영됩니다.",
155
- },
156
- {
157
- pattern: /\bgit\s+rebase\b/i,
158
- warn: "git rebase 감지. 커밋 히스토리가 변경됩니다.",
159
- },
160
- { pattern: /\bgit\s+branch\s+-[dD]\b/i, warn: "브랜치 삭제 감지." },
161
- {
162
- pattern: /\bnpm\s+publish\b/i,
163
- warn: "npm publish 감지. 공개 레지스트리에 배포됩니다.",
164
- },
165
- {
166
- pattern: /\brm\s+(-[^\s]*)?-rf?\s/i,
167
- warn: "재귀 삭제 감지. 대상을 확인하세요.",
168
- },
169
- {
170
- pattern: /--no-verify\b/i,
171
- warn: "--no-verify 감지. 훅 건너뛰기는 권장하지 않습니다.",
172
- },
173
- { pattern: /\bchmod\s+777\b/i, warn: "chmod 777 감지. 보안 위험." },
174
- {
175
- pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i,
176
- warn: "curl | sh 감지. 원격 스크립트 실행 주의.",
177
- },
178
- ];
179
-
180
- // ── reflexion 적응형 패널티 로드 ──────────────────────────────
181
- function loadReflexionPenalties() {
182
- try {
183
- const home = process.env.HOME || process.env.USERPROFILE || "";
184
- const penaltyFile = join(
185
- home,
186
- ".triflux",
187
- "reflexion",
188
- "pending-penalties.jsonl",
189
- );
190
- if (!existsSync(penaltyFile)) return [];
191
- return readFileSync(penaltyFile, "utf8")
192
- .split("\n")
193
- .filter(Boolean)
194
- .map((line) => {
195
- try {
196
- return JSON.parse(line);
197
- } catch {
198
- return null;
199
- }
200
- })
201
- .filter(Boolean);
202
- } catch {
203
- return [];
204
- }
205
- }
206
-
207
- function readStdin() {
208
- try {
209
- return readFileSync(0, "utf8");
210
- } catch {
211
- return "";
212
- }
213
- }
214
-
215
- function shouldSkipSegment(segment) {
216
- return (
217
- !segment ||
218
- segment.startsWith("#") ||
219
- /^\s*(echo|printf|grep|git\s+commit)\b/i.test(segment)
220
- );
221
- }
222
-
223
- function hasSegmentInvocation(cmd, patterns) {
224
- if (!patterns.some((pattern) => pattern.test(cmd))) return false;
225
-
226
- const lines = cmd.split(/\n/);
227
- let heredocDelimiter = null;
228
- return lines.some((line) => {
229
- if (heredocDelimiter !== null) {
230
- if (line.trim() === heredocDelimiter) heredocDelimiter = null;
231
- return false;
232
- }
233
-
234
- const heredocMatch = line.match(/<<['"]?(\w+)['"]?/);
235
- if (heredocMatch) {
236
- heredocDelimiter = heredocMatch[1];
237
- return false;
238
- }
239
-
240
- const segments = line.split(/\s*(?:&&|;|\|\|)\s*/);
241
- return segments.some((seg) => {
242
- const trimmed = seg.trim();
243
- if (shouldSkipSegment(trimmed)) return false;
244
- return patterns.some((pattern) => pattern.test(trimmed));
245
- });
246
- });
247
- }
248
-
249
- function isGitCommitInvocation(command) {
250
- const lines = command.split(/\n/);
251
- let heredocDelimiter = null;
252
- return lines.some((line) => {
253
- if (heredocDelimiter !== null) {
254
- if (line.trim() === heredocDelimiter) heredocDelimiter = null;
255
- return false;
256
- }
257
-
258
- const heredocMatch = line.match(/<<['"]?(\w+)['"]?/);
259
- if (heredocMatch) {
260
- heredocDelimiter = heredocMatch[1];
261
- }
262
-
263
- return line
264
- .split(/\s*(?:&&|;|\|\|)\s*/)
265
- .some((segment) => /^\s*git\s+commit\b/i.test(segment.trim()));
266
- });
267
- }
268
-
269
- function resolveHookCwd(input) {
270
- return String(
271
- input?.cwd ||
272
- input?.tool_input?.cwd ||
273
- process.env.CLAUDE_CWD ||
274
- process.cwd() ||
275
- process.env.PWD,
276
- );
277
- }
278
-
279
- function getCurrentGitBranch(cwd) {
280
- try {
281
- const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
282
- cwd,
283
- encoding: "utf8",
284
- stdio: ["ignore", "pipe", "ignore"],
285
- });
286
- if (result.status !== 0) return "";
287
- return String(result.stdout || "").trim();
288
- } catch {
289
- return "";
290
- }
291
- }
292
-
293
- function isProtectedMainBranch(branch) {
294
- return /^(main|master)$/i.test(String(branch || "").trim());
295
- }
296
-
297
- function isSwarmWorktreeCwd(cwd) {
298
- return /(?:^|\/)\.codex-swarm\/wt-[^/]+(?:\/|$)/.test(
299
- String(cwd || "").replace(/\\/g, "/"),
300
- );
301
- }
302
-
303
- function blockCommand(message, command) {
304
- process.stderr.write(
305
- `${message}\n` +
306
- `명령어: ${command.slice(0, 120)}${command.length > 120 ? "..." : ""}\n` +
307
- "이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요.",
308
- );
309
- process.exit(2);
310
- }
311
-
312
- function main() {
313
- const raw = readStdin();
314
- if (!raw.trim()) process.exit(0);
315
-
316
- let input;
317
- try {
318
- input = JSON.parse(raw);
319
- } catch {
320
- process.exit(0);
321
- }
322
-
323
- if (input.tool_name !== "Bash") process.exit(0);
324
-
325
- const command = (input.tool_input?.command || "").trim();
326
- if (!command) process.exit(0);
327
- const hookCwd = resolveHookCwd(input);
328
-
329
- // psmux 명령이 실제 CLI 호출인지 판별 (오탐 방지)
330
- // git commit 메시지, echo, grep, cat, heredoc 안의 텍스트는 무시
331
- function isPsmuxInvocation(cmd) {
332
- return hasSegmentInvocation(cmd, [/\bpsmux\s+kill-(session|server)\b/i]);
333
- }
334
-
335
- function isAllowedPsmuxWrapperInvocation(cmd) {
336
- return hasSegmentInvocation(cmd, PSMUX_INTERNAL_WRAPPER_PATTERNS);
337
- }
338
-
339
- function isWtDirectInvocation(cmd) {
340
- return hasSegmentInvocation(cmd, WT_DIRECT_PATTERNS);
341
- }
342
-
343
- // wt.exe는 Windows Terminal 전용. macOS/Linux에서는 PATH에 존재하지 않아
344
- // 차단해도 dormant이며, 문서/로그 텍스트의 wt 단어가 false positive로 잡힐
345
- // 위험만 있다. win32에서만 차단 적용.
346
- // SAFETY_GUARD_FORCE_PLATFORM env로 테스트에서 platform 시뮬레이션 가능
347
- // (mac CI에서 win32 차단 동작을 검증하기 위함).
348
- const guardPlatform =
349
- process.env.SAFETY_GUARD_FORCE_PLATFORM || process.platform;
350
- if (guardPlatform === "win32" && isWtDirectInvocation(command)) {
351
- blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
352
- }
353
-
354
- if (isAllowedPsmuxWrapperInvocation(command)) {
355
- process.exit(0);
356
- }
357
-
358
- const codexPrdActive = process.env.CODEX_PRD_ACTIVE === "1";
359
- if (codexPrdActive && isGitCommitInvocation(command)) {
360
- const branch = getCurrentGitBranch(hookCwd);
361
- if (isProtectedMainBranch(branch)) {
362
- const locationHint = isSwarmWorktreeCwd(hookCwd)
363
- ? "swarm worktree 내부에서도 main/master 직접 commit은 금지됩니다."
364
- : "현재 PWD가 swarm worktree가 아닙니다.";
365
- blockCommand(
366
- `[safety-guard] Codex PRD 실행 중 ${branch} 직접 commit 차단됨. ${locationHint}`,
367
- command,
368
- );
369
- }
370
- }
371
-
372
- // 0.1. reflexion 적응형 패널티 — 이전 세션에서 차단된 패턴 사전 경고
373
- const penalties = loadReflexionPenalties();
374
- if (penalties.length > 0) {
375
- for (const penalty of penalties) {
376
- if (
377
- penalty.error_pattern &&
378
- new RegExp(
379
- penalty.error_pattern
380
- .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
381
- .slice(0, 80),
382
- "i",
383
- ).test(command)
384
- ) {
385
- const output = {
386
- hookSpecificOutput: {
387
- hookEventName: "PreToolUse",
388
- permissionDecision: "allow",
389
- additionalContext:
390
- `[reflexion] 이전 세션에서 차단된 패턴과 유사합니다 (${penalty.source}, ${penalty.ts?.slice(0, 10)}). ` +
391
- `이전 차단 사유: ${penalty.error_pattern?.slice(0, 100)}`,
392
- },
393
- };
394
- process.stdout.write(JSON.stringify(output));
395
- process.exit(0);
396
- }
397
- }
398
- }
399
-
400
- // 0.5. SSH → Windows(PowerShell) 호스트에만 bash 문법 전달 차단
401
- // macOS/Linux 대상은 bash/zsh이므로 허용. hosts.json OS로 판별.
402
- if (
403
- hasSegmentInvocation(command, [/^\s*ssh\s+/i]) &&
404
- isSshTargetWindows(command)
405
- ) {
406
- const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
407
- for (const seg of segments) {
408
- const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
409
- if (!sshMatch) continue;
410
- const sshPayload = sshMatch[1];
411
- const bashSyntax = BASH_SYNTAX_IN_SSH.find((p) => p.test(sshPayload));
412
- if (bashSyntax) {
413
- blockCommand(
414
- `[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
415
- command,
416
- );
417
- }
418
- }
419
- }
420
-
421
- // 1. BLOCK 체크 — exit 2로 차단
422
- for (const rule of BLOCK_RULES) {
423
- if (rule.cleanupBypass && CLEANUP_BYPASS) continue;
424
- if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
425
- if (rule.pattern.test(command)) {
426
- blockCommand(`[triflux safety-guard] BLOCKED: ${rule.reason}`, command);
427
- }
428
- }
429
-
430
- // wt 정리 명령 우회 (TFX_CLEANUP_BYPASS=1 한정). new-tab/split-pane만 차단 유지하려면 아래 조건 세분화.
431
- if (CLEANUP_BYPASS) {
432
- // bypass 모드에서는 아래 wt 검사를 이미 통과한 상태. 추가 작업 없음.
433
- }
434
-
435
- // 2. WARN 체크 — allow + additionalContext
436
- const warnings = [];
437
- for (const rule of WARN_RULES) {
438
- if (rule.pattern.test(command)) {
439
- warnings.push(rule.warn);
440
- }
441
- }
442
-
443
- if (warnings.length > 0) {
444
- const output = {
445
- hookSpecificOutput: {
446
- hookEventName: "PreToolUse",
447
- permissionDecision: "allow",
448
- additionalContext:
449
- `[safety-guard] ⚠ ${warnings.join(" | ")}\n` +
450
- `명령어: ${command.slice(0, 200)}`,
451
- },
452
- };
453
- process.stdout.write(JSON.stringify(output));
454
- process.exit(0);
455
- }
456
-
457
- // 3. 안전한 명령 → 통과
458
- process.exit(0);
459
- }
460
-
461
- try {
462
- main();
463
- } catch {
464
- // 훅 실패 시 블로킹하지 않음
465
- process.exit(0);
466
- }