@triflux/core 10.1.0 → 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 +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  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 +254 -33
  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 +831 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  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 -18
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +167 -38
  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 +285 -111
  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 +87 -13
  75. package/hub/research.mjs +33 -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 +127 -61
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +72 -71
  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 +391 -247
  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 +1281 -115
  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 +70 -10
  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 +32 -7
  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/safety-guard.mjs — PreToolUse:Bash 훅
3
4
  //
4
5
  // 위험한 Bash 명령을 사전 차단(exit 2)하거나 경고(additionalContext)한다.
@@ -8,23 +9,71 @@
8
9
  // BLOCK (exit 2) — 복구 불가능한 파괴적 명령
9
10
  // WARN (allow + context) — 주의가 필요한 명령
10
11
 
11
- import { readFileSync, existsSync } from "node:fs";
12
+ import { spawnSync } from "node:child_process";
13
+ import { existsSync, readFileSync } from "node:fs";
12
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
+ })();
13
32
 
14
33
  // ── 차단 규칙 ──────────────────────────────────────────────
15
34
  const BLOCK_RULES = [
16
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
17
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i, reason: "현재 디렉토리 rm -rf . 차단" },
18
- { pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i, reason: "main/master force push 차단" },
19
- { pattern: /\bgit\s+push\s+--force\s*$/i, reason: "대상 미지정 force push 차단" },
20
- { pattern: /\bgit\s+reset\s+--hard\s+origin\//i, reason: "remote reset --hard 차단 — 로컬 작업 소실 위험" },
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
+ },
21
55
  { pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "SQL DROP 차단" },
22
56
  { pattern: /\btruncate\s+table\b/i, reason: "SQL TRUNCATE 차단" },
23
57
  { pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
24
58
  { pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
25
- { pattern: /\bgit\s+clean\s+.*-fd/i, reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험" },
26
- { pattern: /\bpsmux\s+kill-session\b/i, reason: "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>", skipIfGit: true },
27
- { pattern: /\bpsmux\s+kill-server\b/i, reason: "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용", skipIfGit: true },
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
+ },
28
77
  ];
29
78
 
30
79
  const WT_DIRECT_PATTERNS = [
@@ -37,47 +86,122 @@ const WT_DIRECT_PATTERNS = [
37
86
  ];
38
87
 
39
88
  const WT_DIRECT_BLOCK_MESSAGE =
40
- "[safety-guard] wt.exe 직접 호출 차단됨. → hub/team/wt-manager.mjs의 createTab() / splitPane() / applySplitLayout()을 사용하세요.";
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
+ ];
41
99
 
42
100
  // ── SSH+PowerShell bash 문법 차단 ────────────────────────────
43
101
  // 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
102
+ // macOS/Linux 대상 SSH에는 bash 문법이 정상이므로 hosts.json OS를 확인한다.
44
103
  const BASH_SYNTAX_IN_SSH = [
45
- /2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
46
- />\s*\/dev\/null/, // >/dev/null
47
- /&>\s*\/dev\/null/, // &>/dev/null
48
- /\$\(/, // $(cmd) → PowerShell에서 다른 의미
49
- /\bsource\s+/, // source → PowerShell에 없음
50
- /\bexport\s+\w+=/, // export VAR= → PowerShell에 없음
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에 없음
51
110
  ];
52
111
 
53
112
  const SSH_POWERSHELL_HINT =
54
113
  "원격 셸이 PowerShell입니다. bash 문법 직접 전달 금지. scp + pwsh -File 패턴 사용. " +
55
114
  "2>/dev/null → 2>$null, $() → $(), export → $env:, source → . (dot-source)";
56
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
+
57
150
  // ── 경고 규칙 ──────────────────────────────────────────────
58
151
  const WARN_RULES = [
59
- { pattern: /\bgit\s+push\b(?!.*--force)/i, warn: "git push 감지. 원격 저장소에 반영됩니다." },
60
- { pattern: /\bgit\s+rebase\b/i, warn: "git rebase 감지. 커밋 히스토리가 변경됩니다." },
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
+ },
61
160
  { pattern: /\bgit\s+branch\s+-[dD]\b/i, warn: "브랜치 삭제 감지." },
62
- { pattern: /\bnpm\s+publish\b/i, warn: "npm publish 감지. 공개 레지스트리에 배포됩니다." },
63
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s/i, warn: "재귀 삭제 감지. 대상을 확인하세요." },
64
- { pattern: /--no-verify\b/i, warn: "--no-verify 감지. 훅 건너뛰기는 권장하지 않습니다." },
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
+ },
65
173
  { pattern: /\bchmod\s+777\b/i, warn: "chmod 777 감지. 보안 위험." },
66
- { pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i, warn: "curl | sh 감지. 원격 스크립트 실행 주의." },
174
+ {
175
+ pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i,
176
+ warn: "curl | sh 감지. 원격 스크립트 실행 주의.",
177
+ },
67
178
  ];
68
179
 
69
180
  // ── reflexion 적응형 패널티 로드 ──────────────────────────────
70
181
  function loadReflexionPenalties() {
71
182
  try {
72
183
  const home = process.env.HOME || process.env.USERPROFILE || "";
73
- const penaltyFile = join(home, ".triflux", "reflexion", "pending-penalties.jsonl");
184
+ const penaltyFile = join(
185
+ home,
186
+ ".triflux",
187
+ "reflexion",
188
+ "pending-penalties.jsonl",
189
+ );
74
190
  if (!existsSync(penaltyFile)) return [];
75
191
  return readFileSync(penaltyFile, "utf8")
76
192
  .split("\n")
77
193
  .filter(Boolean)
78
- .map(line => { try { return JSON.parse(line); } catch { return null; } })
194
+ .map((line) => {
195
+ try {
196
+ return JSON.parse(line);
197
+ } catch {
198
+ return null;
199
+ }
200
+ })
79
201
  .filter(Boolean);
80
- } catch { return []; }
202
+ } catch {
203
+ return [];
204
+ }
81
205
  }
82
206
 
83
207
  function readStdin() {
@@ -89,7 +213,11 @@ function readStdin() {
89
213
  }
90
214
 
91
215
  function shouldSkipSegment(segment) {
92
- return !segment || segment.startsWith("#") || /^\s*(echo|printf|grep|git\s+commit)\b/i.test(segment);
216
+ return (
217
+ !segment ||
218
+ segment.startsWith("#") ||
219
+ /^\s*(echo|printf|grep|git\s+commit)\b/i.test(segment)
220
+ );
93
221
  }
94
222
 
95
223
  function hasSegmentInvocation(cmd, patterns) {
@@ -118,11 +246,65 @@ function hasSegmentInvocation(cmd, patterns) {
118
246
  });
119
247
  }
120
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
+
121
303
  function blockCommand(message, command) {
122
304
  process.stderr.write(
123
305
  `${message}\n` +
124
306
  `명령어: ${command.slice(0, 120)}${command.length > 120 ? "..." : ""}\n` +
125
- "이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요."
307
+ "이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요.",
126
308
  );
127
309
  process.exit(2);
128
310
  }
@@ -142,6 +324,7 @@ function main() {
142
324
 
143
325
  const command = (input.tool_input?.command || "").trim();
144
326
  if (!command) process.exit(0);
327
+ const hookCwd = resolveHookCwd(input);
145
328
 
146
329
  // psmux 명령이 실제 CLI 호출인지 판별 (오탐 방지)
147
330
  // git commit 메시지, echo, grep, cat, heredoc 안의 텍스트는 무시
@@ -149,6 +332,10 @@ function main() {
149
332
  return hasSegmentInvocation(cmd, [/\bpsmux\s+kill-(session|server)\b/i]);
150
333
  }
151
334
 
335
+ function isAllowedPsmuxWrapperInvocation(cmd) {
336
+ return hasSegmentInvocation(cmd, PSMUX_INTERNAL_WRAPPER_PATTERNS);
337
+ }
338
+
152
339
  function isWtDirectInvocation(cmd) {
153
340
  return hasSegmentInvocation(cmd, WT_DIRECT_PATTERNS);
154
341
  }
@@ -157,11 +344,37 @@ function main() {
157
344
  blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
158
345
  }
159
346
 
347
+ if (isAllowedPsmuxWrapperInvocation(command)) {
348
+ process.exit(0);
349
+ }
350
+
351
+ const codexPrdActive = process.env.CODEX_PRD_ACTIVE === "1";
352
+ if (codexPrdActive && isGitCommitInvocation(command)) {
353
+ const branch = getCurrentGitBranch(hookCwd);
354
+ if (isProtectedMainBranch(branch)) {
355
+ const locationHint = isSwarmWorktreeCwd(hookCwd)
356
+ ? "swarm worktree 내부에서도 main/master 직접 commit은 금지됩니다."
357
+ : "현재 PWD가 swarm worktree가 아닙니다.";
358
+ blockCommand(
359
+ `[safety-guard] Codex PRD 실행 중 ${branch} 직접 commit 차단됨. ${locationHint}`,
360
+ command,
361
+ );
362
+ }
363
+ }
364
+
160
365
  // 0.1. reflexion 적응형 패널티 — 이전 세션에서 차단된 패턴 사전 경고
161
366
  const penalties = loadReflexionPenalties();
162
367
  if (penalties.length > 0) {
163
368
  for (const penalty of penalties) {
164
- if (penalty.error_pattern && new RegExp(penalty.error_pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 80), "i").test(command)) {
369
+ if (
370
+ penalty.error_pattern &&
371
+ new RegExp(
372
+ penalty.error_pattern
373
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
374
+ .slice(0, 80),
375
+ "i",
376
+ ).test(command)
377
+ ) {
165
378
  const output = {
166
379
  hookSpecificOutput: {
167
380
  hookEventName: "PreToolUse",
@@ -177,16 +390,18 @@ function main() {
177
390
  }
178
391
  }
179
392
 
180
- // 0.5. SSH → PowerShell 호스트에 bash 문법 전달 차단
181
- // 세그먼트 시작 위치에서 ssh 명령인 경우만 (문자열/코드 안의 ssh는 무시)
182
- if (hasSegmentInvocation(command, [/^\s*ssh\s+/i])) {
183
- // 세그먼트 분리 후 ssh로 시작하는 세그먼트의 payload만 검사
393
+ // 0.5. SSH → Windows(PowerShell) 호스트에만 bash 문법 전달 차단
394
+ // macOS/Linux 대상은 bash/zsh이므로 허용. hosts.json OS로 판별.
395
+ if (
396
+ hasSegmentInvocation(command, [/^\s*ssh\s+/i]) &&
397
+ isSshTargetWindows(command)
398
+ ) {
184
399
  const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
185
400
  for (const seg of segments) {
186
401
  const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
187
402
  if (!sshMatch) continue;
188
403
  const sshPayload = sshMatch[1];
189
- const bashSyntax = BASH_SYNTAX_IN_SSH.find(p => p.test(sshPayload));
404
+ const bashSyntax = BASH_SYNTAX_IN_SSH.find((p) => p.test(sshPayload));
190
405
  if (bashSyntax) {
191
406
  blockCommand(
192
407
  `[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
@@ -198,12 +413,18 @@ function main() {
198
413
 
199
414
  // 1. BLOCK 체크 — exit 2로 차단
200
415
  for (const rule of BLOCK_RULES) {
416
+ if (rule.cleanupBypass && CLEANUP_BYPASS) continue;
201
417
  if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
202
418
  if (rule.pattern.test(command)) {
203
419
  blockCommand(`[triflux safety-guard] BLOCKED: ${rule.reason}`, command);
204
420
  }
205
421
  }
206
422
 
423
+ // wt 정리 명령 우회 (TFX_CLEANUP_BYPASS=1 한정). new-tab/split-pane만 차단 유지하려면 아래 조건 세분화.
424
+ if (CLEANUP_BYPASS) {
425
+ // bypass 모드에서는 아래 wt 검사를 이미 통과한 상태. 추가 작업 없음.
426
+ }
427
+
207
428
  // 2. WARN 체크 — allow + additionalContext
208
429
  const warnings = [];
209
430
  for (const rule of WARN_RULES) {