@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.
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 +47 -7
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +279 -33
  7. package/hooks/hook-registry.json +104 -7
  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 +308 -18
  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 +832 -291
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -3
  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 -17
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +171 -39
  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 +303 -107
  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 +116 -58
  75. package/hub/research.mjs +34 -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 +129 -63
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +76 -72
  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 +393 -249
  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 +1282 -116
  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 +72 -12
  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 +36 -11
  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,22 +9,71 @@
8
9
  // BLOCK (exit 2) — 복구 불가능한 파괴적 명령
9
10
  // WARN (allow + context) — 주의가 필요한 명령
10
11
 
11
- import { readFileSync } from "node:fs";
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
+ })();
12
32
 
13
33
  // ── 차단 규칙 ──────────────────────────────────────────────
14
34
  const BLOCK_RULES = [
15
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
16
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i, reason: "현재 디렉토리 rm -rf . 차단" },
17
- { pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i, reason: "main/master force push 차단" },
18
- { pattern: /\bgit\s+push\s+--force\s*$/i, reason: "대상 미지정 force push 차단" },
19
- { 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
+ },
20
55
  { pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "SQL DROP 차단" },
21
56
  { pattern: /\btruncate\s+table\b/i, reason: "SQL TRUNCATE 차단" },
22
57
  { pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
23
58
  { pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
24
- { pattern: /\bgit\s+clean\s+.*-fd/i, reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험" },
25
- { pattern: /\bpsmux\s+kill-session\b/i, reason: "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>", skipIfGit: true },
26
- { 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
+ },
27
77
  ];
28
78
 
29
79
  const WT_DIRECT_PATTERNS = [
@@ -36,20 +86,124 @@ const WT_DIRECT_PATTERNS = [
36
86
  ];
37
87
 
38
88
  const WT_DIRECT_BLOCK_MESSAGE =
39
- "[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
+ ];
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
+ }
40
149
 
41
150
  // ── 경고 규칙 ──────────────────────────────────────────────
42
151
  const WARN_RULES = [
43
- { pattern: /\bgit\s+push\b(?!.*--force)/i, warn: "git push 감지. 원격 저장소에 반영됩니다." },
44
- { 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
+ },
45
160
  { pattern: /\bgit\s+branch\s+-[dD]\b/i, warn: "브랜치 삭제 감지." },
46
- { pattern: /\bnpm\s+publish\b/i, warn: "npm publish 감지. 공개 레지스트리에 배포됩니다." },
47
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s/i, warn: "재귀 삭제 감지. 대상을 확인하세요." },
48
- { 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
+ },
49
173
  { pattern: /\bchmod\s+777\b/i, warn: "chmod 777 감지. 보안 위험." },
50
- { 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
+ },
51
178
  ];
52
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
+
53
207
  function readStdin() {
54
208
  try {
55
209
  return readFileSync(0, "utf8");
@@ -59,7 +213,11 @@ function readStdin() {
59
213
  }
60
214
 
61
215
  function shouldSkipSegment(segment) {
62
- 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
+ );
63
221
  }
64
222
 
65
223
  function hasSegmentInvocation(cmd, patterns) {
@@ -88,11 +246,65 @@ function hasSegmentInvocation(cmd, patterns) {
88
246
  });
89
247
  }
90
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
+
91
303
  function blockCommand(message, command) {
92
304
  process.stderr.write(
93
305
  `${message}\n` +
94
306
  `명령어: ${command.slice(0, 120)}${command.length > 120 ? "..." : ""}\n` +
95
- "이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요."
307
+ "이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요.",
96
308
  );
97
309
  process.exit(2);
98
310
  }
@@ -112,6 +324,7 @@ function main() {
112
324
 
113
325
  const command = (input.tool_input?.command || "").trim();
114
326
  if (!command) process.exit(0);
327
+ const hookCwd = resolveHookCwd(input);
115
328
 
116
329
  // psmux 명령이 실제 CLI 호출인지 판별 (오탐 방지)
117
330
  // git commit 메시지, echo, grep, cat, heredoc 안의 텍스트는 무시
@@ -119,6 +332,10 @@ function main() {
119
332
  return hasSegmentInvocation(cmd, [/\bpsmux\s+kill-(session|server)\b/i]);
120
333
  }
121
334
 
335
+ function isAllowedPsmuxWrapperInvocation(cmd) {
336
+ return hasSegmentInvocation(cmd, PSMUX_INTERNAL_WRAPPER_PATTERNS);
337
+ }
338
+
122
339
  function isWtDirectInvocation(cmd) {
123
340
  return hasSegmentInvocation(cmd, WT_DIRECT_PATTERNS);
124
341
  }
@@ -127,14 +344,87 @@ function main() {
127
344
  blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
128
345
  }
129
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
+
365
+ // 0.1. reflexion 적응형 패널티 — 이전 세션에서 차단된 패턴 사전 경고
366
+ const penalties = loadReflexionPenalties();
367
+ if (penalties.length > 0) {
368
+ for (const penalty of penalties) {
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
+ ) {
378
+ const output = {
379
+ hookSpecificOutput: {
380
+ hookEventName: "PreToolUse",
381
+ permissionDecision: "allow",
382
+ additionalContext:
383
+ `[reflexion] 이전 세션에서 차단된 패턴과 유사합니다 (${penalty.source}, ${penalty.ts?.slice(0, 10)}). ` +
384
+ `이전 차단 사유: ${penalty.error_pattern?.slice(0, 100)}`,
385
+ },
386
+ };
387
+ process.stdout.write(JSON.stringify(output));
388
+ process.exit(0);
389
+ }
390
+ }
391
+ }
392
+
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
+ ) {
399
+ const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
400
+ for (const seg of segments) {
401
+ const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
402
+ if (!sshMatch) continue;
403
+ const sshPayload = sshMatch[1];
404
+ const bashSyntax = BASH_SYNTAX_IN_SSH.find((p) => p.test(sshPayload));
405
+ if (bashSyntax) {
406
+ blockCommand(
407
+ `[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
408
+ command,
409
+ );
410
+ }
411
+ }
412
+ }
413
+
130
414
  // 1. BLOCK 체크 — exit 2로 차단
131
415
  for (const rule of BLOCK_RULES) {
416
+ if (rule.cleanupBypass && CLEANUP_BYPASS) continue;
132
417
  if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
133
418
  if (rule.pattern.test(command)) {
134
419
  blockCommand(`[triflux safety-guard] BLOCKED: ${rule.reason}`, command);
135
420
  }
136
421
  }
137
422
 
423
+ // wt 정리 명령 우회 (TFX_CLEANUP_BYPASS=1 한정). new-tab/split-pane만 차단 유지하려면 아래 조건 세분화.
424
+ if (CLEANUP_BYPASS) {
425
+ // bypass 모드에서는 아래 wt 검사를 이미 통과한 상태. 추가 작업 없음.
426
+ }
427
+
138
428
  // 2. WARN 체크 — allow + additionalContext
139
429
  const warnings = [];
140
430
  for (const rule of WARN_RULES) {