@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.
Files changed (145) 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 +191 -31
  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 +262 -34
  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 +1051 -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 +22 -21
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/schema/delegator-tools.schema.json +1 -1
  35. package/hub/delegator/service.mjs +93 -64
  36. package/hub/delegator/tool-definitions.mjs +35 -35
  37. package/hub/dynamic-routing-engine.mjs +511 -0
  38. package/hub/fullcycle.mjs +34 -18
  39. package/hub/gemini-adapter.mjs +82 -108
  40. package/hub/hitl.mjs +202 -143
  41. package/hub/intent.mjs +167 -38
  42. package/hub/lib/bash-path.mjs +73 -0
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/env-detect.mjs +145 -0
  45. package/hub/lib/hosts-compat.mjs +319 -0
  46. package/hub/lib/mcp-response-cache.mjs +239 -0
  47. package/hub/lib/memory-store.mjs +285 -111
  48. package/hub/lib/path-utils.mjs +165 -0
  49. package/hub/lib/phase-manager.mjs +493 -0
  50. package/hub/lib/process-utils.mjs +797 -104
  51. package/hub/lib/prompt-tmp.mjs +33 -0
  52. package/hub/lib/risk-tier.mjs +53 -0
  53. package/hub/lib/spawn-trace.mjs +561 -0
  54. package/hub/lib/ssh-command.mjs +44 -24
  55. package/hub/lib/ssh-retry.mjs +5 -1
  56. package/hub/lib/staged-file-detect.mjs +43 -0
  57. package/hub/lib/state-snapshot.mjs +293 -0
  58. package/hub/lib/tfx-route-args.mjs +309 -0
  59. package/hub/lib/timeline-adapter.mjs +190 -0
  60. package/hub/lib/trace-recorder.mjs +153 -0
  61. package/hub/lib/uuidv7.mjs +4 -3
  62. package/hub/memory-doctor.mjs +266 -106
  63. package/hub/middleware/quota-middleware.mjs +70 -0
  64. package/hub/middleware/request-logger.mjs +61 -34
  65. package/hub/paths.mjs +9 -9
  66. package/hub/pipeline/gates/confidence.mjs +34 -15
  67. package/hub/pipeline/gates/consensus.mjs +27 -15
  68. package/hub/pipeline/gates/index.mjs +7 -3
  69. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  70. package/hub/pipeline/index.mjs +77 -42
  71. package/hub/pipeline/state.mjs +10 -10
  72. package/hub/pipeline/transitions.mjs +40 -23
  73. package/hub/platform.mjs +65 -48
  74. package/hub/quality/deslop.mjs +70 -49
  75. package/hub/reflexion.mjs +87 -13
  76. package/hub/research.mjs +33 -25
  77. package/hub/router.mjs +941 -791
  78. package/hub/routing/complexity.mjs +132 -29
  79. package/hub/routing/index.mjs +17 -12
  80. package/hub/routing/q-learning.mjs +78 -30
  81. package/hub/routing-snapshot.mjs +261 -0
  82. package/hub/session-fingerprint.mjs +127 -61
  83. package/hub/state.mjs +86 -44
  84. package/hub/team/claude-daemon-control.mjs +1130 -0
  85. package/hub/team/retry-state-machine.mjs +336 -0
  86. package/hub/token-mode.mjs +72 -71
  87. package/hub/workers/worker-utils.mjs +23 -17
  88. package/hud/colors.mjs +27 -9
  89. package/hud/constants.mjs +190 -27
  90. package/hud/context-monitor.mjs +133 -44
  91. package/hud/hud-qos-status.mjs +157 -50
  92. package/hud/mission-board.mjs +56 -0
  93. package/hud/providers/claude.mjs +398 -114
  94. package/hud/providers/codex.mjs +96 -21
  95. package/hud/providers/gemini.mjs +210 -52
  96. package/hud/renderers.mjs +440 -127
  97. package/hud/terminal.mjs +101 -31
  98. package/hud/utils.mjs +81 -39
  99. package/mesh/index.mjs +69 -0
  100. package/mesh/mesh-budget.mjs +137 -0
  101. package/mesh/mesh-heartbeat.mjs +100 -0
  102. package/mesh/mesh-protocol.mjs +96 -0
  103. package/mesh/mesh-queue.mjs +163 -0
  104. package/mesh/mesh-registry.mjs +78 -0
  105. package/mesh/mesh-router.mjs +77 -0
  106. package/package.json +5 -1
  107. package/scripts/lib/agent-json.mjs +27 -0
  108. package/scripts/lib/async.mjs +174 -0
  109. package/scripts/lib/claudemd-scanner.mjs +6 -1
  110. package/scripts/lib/cli-agy.mjs +62 -0
  111. package/scripts/lib/cli-claude.mjs +78 -0
  112. package/scripts/lib/cli-codex.mjs +199 -0
  113. package/scripts/lib/cli-gemini.mjs +37 -0
  114. package/scripts/lib/codex-recovery.sh +50 -0
  115. package/scripts/lib/context.mjs +3 -3
  116. package/scripts/lib/cross-review-utils.mjs +13 -5
  117. package/scripts/lib/doctor-env-checks.mjs +121 -0
  118. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  119. package/scripts/lib/env-probe.mjs +58 -30
  120. package/scripts/lib/env.mjs +14 -0
  121. package/scripts/lib/gemini-profiles.mjs +44 -10
  122. package/scripts/lib/handoff.mjs +33 -17
  123. package/scripts/lib/hook-utils.mjs +8 -6
  124. package/scripts/lib/hub.mjs +48 -0
  125. package/scripts/lib/keyword-rules.mjs +49 -20
  126. package/scripts/lib/logger.mjs +24 -24
  127. package/scripts/lib/mcp-filter.mjs +391 -247
  128. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  129. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  130. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  131. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  132. package/scripts/lib/mcp-health.mjs +773 -0
  133. package/scripts/lib/mcp-manifest.mjs +24 -14
  134. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  135. package/scripts/lib/pid.mjs +63 -0
  136. package/scripts/lib/process-utils.mjs +26 -0
  137. package/scripts/lib/psmux-info.mjs +117 -12
  138. package/scripts/lib/quota.mjs +47 -0
  139. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  140. package/scripts/lib/skill-state.mjs +220 -0
  141. package/scripts/lib/skill-template.mjs +32 -7
  142. package/scripts/lib/team.mjs +75 -0
  143. package/scripts/lib/timeout.mjs +66 -0
  144. package/scripts/lib/tmp.mjs +26 -0
  145. 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,19 +332,56 @@ 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
  }
155
342
 
156
- if (isWtDirectInvocation(command)) {
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)) {
157
351
  blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
158
352
  }
159
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
+
160
372
  // 0.1. reflexion 적응형 패널티 — 이전 세션에서 차단된 패턴 사전 경고
161
373
  const penalties = loadReflexionPenalties();
162
374
  if (penalties.length > 0) {
163
375
  for (const penalty of penalties) {
164
- if (penalty.error_pattern && new RegExp(penalty.error_pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").slice(0, 80), "i").test(command)) {
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
+ ) {
165
385
  const output = {
166
386
  hookSpecificOutput: {
167
387
  hookEventName: "PreToolUse",
@@ -177,16 +397,18 @@ function main() {
177
397
  }
178
398
  }
179
399
 
180
- // 0.5. SSH → PowerShell 호스트에 bash 문법 전달 차단
181
- // 세그먼트 시작 위치에서 ssh 명령인 경우만 (문자열/코드 안의 ssh는 무시)
182
- if (hasSegmentInvocation(command, [/^\s*ssh\s+/i])) {
183
- // 세그먼트 분리 후 ssh로 시작하는 세그먼트의 payload만 검사
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
+ ) {
184
406
  const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
185
407
  for (const seg of segments) {
186
408
  const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
187
409
  if (!sshMatch) continue;
188
410
  const sshPayload = sshMatch[1];
189
- const bashSyntax = BASH_SYNTAX_IN_SSH.find(p => p.test(sshPayload));
411
+ const bashSyntax = BASH_SYNTAX_IN_SSH.find((p) => p.test(sshPayload));
190
412
  if (bashSyntax) {
191
413
  blockCommand(
192
414
  `[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
@@ -198,12 +420,18 @@ function main() {
198
420
 
199
421
  // 1. BLOCK 체크 — exit 2로 차단
200
422
  for (const rule of BLOCK_RULES) {
423
+ if (rule.cleanupBypass && CLEANUP_BYPASS) continue;
201
424
  if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
202
425
  if (rule.pattern.test(command)) {
203
426
  blockCommand(`[triflux safety-guard] BLOCKED: ${rule.reason}`, command);
204
427
  }
205
428
  }
206
429
 
430
+ // wt 정리 명령 우회 (TFX_CLEANUP_BYPASS=1 한정). new-tab/split-pane만 차단 유지하려면 아래 조건 세분화.
431
+ if (CLEANUP_BYPASS) {
432
+ // bypass 모드에서는 아래 wt 검사를 이미 통과한 상태. 추가 작업 없음.
433
+ }
434
+
207
435
  // 2. WARN 체크 — allow + additionalContext
208
436
  const warnings = [];
209
437
  for (const rule of WARN_RULES) {