triflux 10.3.2 → 10.3.4

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 (65) hide show
  1. package/.claude-plugin/plugin.json +22 -22
  2. package/LICENSE +21 -21
  3. package/README.ko.md +16 -0
  4. package/README.md +8 -0
  5. package/hooks/hook-registry.json +256 -256
  6. package/hub/adaptive-inject.mjs +1 -1
  7. package/hub/assign-callbacks.mjs +120 -120
  8. package/hub/delegator/index.mjs +14 -14
  9. package/hub/delegator/tool-definitions.mjs +35 -35
  10. package/hub/hitl.mjs +143 -143
  11. package/hub/lib/path-utils.mjs +167 -0
  12. package/hub/router.mjs +791 -791
  13. package/hub/session-fingerprint.mjs +1 -1
  14. package/hub/team/cli/commands/attach.mjs +37 -37
  15. package/hub/team/cli/commands/debug.mjs +74 -74
  16. package/hub/team/cli/commands/focus.mjs +53 -53
  17. package/hub/team/cli/commands/list.mjs +24 -24
  18. package/hub/team/cli/commands/start/start-in-process.mjs +40 -40
  19. package/hub/team/cli/commands/start/start-mux.mjs +73 -73
  20. package/hub/team/cli/commands/start/start-wt.mjs +69 -69
  21. package/hub/team/cli/commands/tasks.mjs +13 -13
  22. package/hub/team/cli/render.mjs +30 -30
  23. package/hub/team/cli/services/attach-fallback.mjs +54 -54
  24. package/hub/team/cli/services/member-selector.mjs +30 -30
  25. package/hub/team/cli/services/native-control.mjs +116 -116
  26. package/hub/team/cli/services/task-model.mjs +30 -30
  27. package/hub/team/notify.mjs +1 -1
  28. package/hub/team/orchestrator.mjs +161 -161
  29. package/hub/team/runtime-strategy.mjs +74 -0
  30. package/hub/team/session.mjs +611 -611
  31. package/hub/team/shared.mjs +13 -13
  32. package/hub/team/worktree-lifecycle.mjs +61 -2
  33. package/hub/tray.mjs +368 -368
  34. package/hub/workers/codex-mcp.mjs +507 -507
  35. package/hub/workers/factory.mjs +21 -21
  36. package/hud/hud-qos-status.mjs +17 -3
  37. package/hud/mission-board.mjs +53 -0
  38. package/hud/providers/claude.mjs +95 -22
  39. package/hud/renderers.mjs +39 -5
  40. package/mesh/index.mjs +63 -0
  41. package/mesh/mesh-budget.mjs +128 -0
  42. package/mesh/mesh-heartbeat.mjs +100 -0
  43. package/mesh/mesh-protocol.mjs +96 -0
  44. package/mesh/mesh-queue.mjs +165 -0
  45. package/mesh/mesh-registry.mjs +78 -0
  46. package/mesh/mesh-router.mjs +76 -0
  47. package/package.json +2 -1
  48. package/scripts/completions/tfx.bash +47 -47
  49. package/scripts/completions/tfx.fish +44 -44
  50. package/scripts/completions/tfx.zsh +83 -83
  51. package/scripts/demo.mjs +169 -0
  52. package/scripts/headless-guard.mjs +16 -4
  53. package/scripts/hub-ensure.mjs +120 -120
  54. package/scripts/keyword-detector.mjs +272 -272
  55. package/scripts/keyword-rules-expander.mjs +521 -521
  56. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  57. package/scripts/lib/skill-state.mjs +220 -0
  58. package/scripts/notion-read.mjs +553 -553
  59. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  60. package/scripts/tfx-batch-stats.mjs +96 -96
  61. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
  62. package/skills/.omc/state/idle-notif-cooldown.json +0 -3
  63. package/skills/.omc/state/last-tool-error.json +0 -7
  64. package/skills/.omc/state/subagent-tracking.json +0 -7
  65. package/skills/tfx-remote-spawn/references/hosts.json +0 -16
@@ -1,44 +1,44 @@
1
- # Installation: ~/.config/fish/completions/에 복사
2
- # e.g., cp /path/to/tfx.fish ~/.config/fish/completions/tfx.fish
3
-
4
- set -l commands setup doctor multi hub auto codex gemini
5
- set -l multi_cmds status stop kill attach list
6
- set -l hub_cmds start stop status restart
7
-
8
- complete -c tfx -f
9
-
10
- # Subcommands
11
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "setup" -d "Setup and sync files"
12
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "doctor" -d "Diagnose CLI and issues"
13
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "multi" -d "Multi-CLI team mode"
14
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "hub" -d "MCP message bus management"
15
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "auto" -d "Auto mode"
16
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "codex" -d "Codex mode"
17
- complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "gemini" -d "Gemini mode"
18
-
19
- # Doctor flags
20
- complete -c tfx -n "__fish_seen_subcommand_from doctor" -l fix -d "Auto fix issues"
21
- complete -c tfx -n "__fish_seen_subcommand_from doctor" -l reset -d "Reset all caches"
22
-
23
- # Multi subcommands
24
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "status"
25
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "stop"
26
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "kill"
27
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "attach"
28
- complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "list"
29
-
30
- # Hub subcommands
31
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "start"
32
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "stop"
33
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "status"
34
- complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "restart"
35
-
36
- # Global or multi flags
37
- set -l flags_cond "__fish_seen_subcommand_from setup multi auto codex gemini"
38
- complete -c tfx -n "$flags_cond" -l thorough -d "Thorough execution"
39
- complete -c tfx -n "$flags_cond" -l quick -d "Quick execution"
40
- complete -c tfx -n "$flags_cond" -l tmux -d "Use tmux"
41
- complete -c tfx -n "$flags_cond" -l psmux -d "Use psmux"
42
- complete -c tfx -n "$flags_cond" -l agents -d "Specify agents"
43
- complete -c tfx -n "$flags_cond" -l no-attach -d "Do not attach"
44
- complete -c tfx -n "$flags_cond" -l timeout -d "Set timeout"
1
+ # Installation: ~/.config/fish/completions/에 복사
2
+ # e.g., cp /path/to/tfx.fish ~/.config/fish/completions/tfx.fish
3
+
4
+ set -l commands setup doctor multi hub auto codex gemini
5
+ set -l multi_cmds status stop kill attach list
6
+ set -l hub_cmds start stop status restart
7
+
8
+ complete -c tfx -f
9
+
10
+ # Subcommands
11
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "setup" -d "Setup and sync files"
12
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "doctor" -d "Diagnose CLI and issues"
13
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "multi" -d "Multi-CLI team mode"
14
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "hub" -d "MCP message bus management"
15
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "auto" -d "Auto mode"
16
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "codex" -d "Codex mode"
17
+ complete -c tfx -n "not __fish_seen_subcommand_from $commands" -a "gemini" -d "Gemini mode"
18
+
19
+ # Doctor flags
20
+ complete -c tfx -n "__fish_seen_subcommand_from doctor" -l fix -d "Auto fix issues"
21
+ complete -c tfx -n "__fish_seen_subcommand_from doctor" -l reset -d "Reset all caches"
22
+
23
+ # Multi subcommands
24
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "status"
25
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "stop"
26
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "kill"
27
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "attach"
28
+ complete -c tfx -n "__fish_seen_subcommand_from multi; and not __fish_seen_subcommand_from $multi_cmds" -a "list"
29
+
30
+ # Hub subcommands
31
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "start"
32
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "stop"
33
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "status"
34
+ complete -c tfx -n "__fish_seen_subcommand_from hub; and not __fish_seen_subcommand_from $hub_cmds" -a "restart"
35
+
36
+ # Global or multi flags
37
+ set -l flags_cond "__fish_seen_subcommand_from setup multi auto codex gemini"
38
+ complete -c tfx -n "$flags_cond" -l thorough -d "Thorough execution"
39
+ complete -c tfx -n "$flags_cond" -l quick -d "Quick execution"
40
+ complete -c tfx -n "$flags_cond" -l tmux -d "Use tmux"
41
+ complete -c tfx -n "$flags_cond" -l psmux -d "Use psmux"
42
+ complete -c tfx -n "$flags_cond" -l agents -d "Specify agents"
43
+ complete -c tfx -n "$flags_cond" -l no-attach -d "Do not attach"
44
+ complete -c tfx -n "$flags_cond" -l timeout -d "Set timeout"
@@ -1,83 +1,83 @@
1
- #compdef tfx
2
- # Installation: fpath에 추가 후 compinit
3
- # e.g., fpath=(/path/to/dir $fpath) && compinit
4
-
5
- _tfx() {
6
- local line state
7
- local -a commands multi_cmds hub_cmds flags
8
-
9
- commands=(
10
- 'setup:Setup and sync files'
11
- 'doctor:Diagnose CLI and issues'
12
- 'multi:Multi-CLI team mode'
13
- 'hub:MCP message bus management'
14
- 'auto:Auto mode'
15
- 'codex:Codex mode'
16
- 'gemini:Gemini mode'
17
- )
18
-
19
- multi_cmds=(
20
- 'status:Show status'
21
- 'stop:Stop multi'
22
- 'kill:Kill multi'
23
- 'attach:Attach to multi'
24
- 'list:List multi sessions'
25
- )
26
-
27
- hub_cmds=(
28
- 'start:Start hub'
29
- 'stop:Stop hub'
30
- 'status:Show hub status'
31
- 'restart:Restart hub'
32
- )
33
-
34
- _arguments -C \
35
- '1: :->cmds' \
36
- '*: :->args'
37
-
38
- case $state in
39
- cmds)
40
- _describe -t commands 'tfx commands' commands
41
- ;;
42
- args)
43
- case $words[2] in
44
- multi)
45
- if (( CURRENT == 3 )) && [[ $words[CURRENT] != -* ]]; then
46
- _describe -t multi_cmds 'multi commands' multi_cmds
47
- else
48
- _arguments \
49
- '--thorough[Thorough execution]' \
50
- '--quick[Quick execution]' \
51
- '--tmux[Use tmux]' \
52
- '--psmux[Use psmux]' \
53
- '--agents[Specify agents]' \
54
- '--no-attach[Do not attach]' \
55
- '--timeout[Set timeout]'
56
- fi
57
- ;;
58
- hub)
59
- if (( CURRENT == 3 )); then
60
- _describe -t hub_cmds 'hub commands' hub_cmds
61
- fi
62
- ;;
63
- doctor)
64
- _arguments \
65
- '--fix[Auto fix issues]' \
66
- '--reset[Reset all caches]'
67
- ;;
68
- *)
69
- _arguments \
70
- '--thorough[Thorough execution]' \
71
- '--quick[Quick execution]' \
72
- '--tmux[Use tmux]' \
73
- '--psmux[Use psmux]' \
74
- '--agents[Specify agents]' \
75
- '--no-attach[Do not attach]' \
76
- '--timeout[Set timeout]'
77
- ;;
78
- esac
79
- ;;
80
- esac
81
- }
82
-
83
- _tfx "$@"
1
+ #compdef tfx
2
+ # Installation: fpath에 추가 후 compinit
3
+ # e.g., fpath=(/path/to/dir $fpath) && compinit
4
+
5
+ _tfx() {
6
+ local line state
7
+ local -a commands multi_cmds hub_cmds flags
8
+
9
+ commands=(
10
+ 'setup:Setup and sync files'
11
+ 'doctor:Diagnose CLI and issues'
12
+ 'multi:Multi-CLI team mode'
13
+ 'hub:MCP message bus management'
14
+ 'auto:Auto mode'
15
+ 'codex:Codex mode'
16
+ 'gemini:Gemini mode'
17
+ )
18
+
19
+ multi_cmds=(
20
+ 'status:Show status'
21
+ 'stop:Stop multi'
22
+ 'kill:Kill multi'
23
+ 'attach:Attach to multi'
24
+ 'list:List multi sessions'
25
+ )
26
+
27
+ hub_cmds=(
28
+ 'start:Start hub'
29
+ 'stop:Stop hub'
30
+ 'status:Show hub status'
31
+ 'restart:Restart hub'
32
+ )
33
+
34
+ _arguments -C \
35
+ '1: :->cmds' \
36
+ '*: :->args'
37
+
38
+ case $state in
39
+ cmds)
40
+ _describe -t commands 'tfx commands' commands
41
+ ;;
42
+ args)
43
+ case $words[2] in
44
+ multi)
45
+ if (( CURRENT == 3 )) && [[ $words[CURRENT] != -* ]]; then
46
+ _describe -t multi_cmds 'multi commands' multi_cmds
47
+ else
48
+ _arguments \
49
+ '--thorough[Thorough execution]' \
50
+ '--quick[Quick execution]' \
51
+ '--tmux[Use tmux]' \
52
+ '--psmux[Use psmux]' \
53
+ '--agents[Specify agents]' \
54
+ '--no-attach[Do not attach]' \
55
+ '--timeout[Set timeout]'
56
+ fi
57
+ ;;
58
+ hub)
59
+ if (( CURRENT == 3 )); then
60
+ _describe -t hub_cmds 'hub commands' hub_cmds
61
+ fi
62
+ ;;
63
+ doctor)
64
+ _arguments \
65
+ '--fix[Auto fix issues]' \
66
+ '--reset[Reset all caches]'
67
+ ;;
68
+ *)
69
+ _arguments \
70
+ '--thorough[Thorough execution]' \
71
+ '--quick[Quick execution]' \
72
+ '--tmux[Use tmux]' \
73
+ '--psmux[Use psmux]' \
74
+ '--agents[Specify agents]' \
75
+ '--no-attach[Do not attach]' \
76
+ '--timeout[Set timeout]'
77
+ ;;
78
+ esac
79
+ ;;
80
+ esac
81
+ }
82
+
83
+ _tfx "$@"
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+
3
+ import childProcess from "node:child_process";
4
+ import { parseArgs } from "node:util";
5
+
6
+ const { values: flags } = parseArgs({
7
+ options: {
8
+ "dry-run": { type: "boolean", default: false },
9
+ keep: { type: "boolean", default: false },
10
+ },
11
+ strict: false,
12
+ });
13
+
14
+ const SESSION_NAME = "triflux-demo";
15
+
16
+ const WORKERS = [
17
+ {
18
+ pane: 0,
19
+ agent: "codex",
20
+ messages: [
21
+ "[codex] Analyzing auth module...",
22
+ "[codex] Refactoring JWT validation...",
23
+ "[codex] Done ✓",
24
+ ],
25
+ },
26
+ {
27
+ pane: 1,
28
+ agent: "gemini",
29
+ messages: [
30
+ "[gemini] Reviewing UI components...",
31
+ "[gemini] Optimizing render cycle...",
32
+ "[gemini] Done ✓",
33
+ ],
34
+ },
35
+ {
36
+ pane: 2,
37
+ agent: "claude",
38
+ messages: [
39
+ "[claude] Security audit in progress...",
40
+ "[claude] Found 0 vulnerabilities",
41
+ "[claude] Done ✓",
42
+ ],
43
+ },
44
+ ];
45
+
46
+ export function checkPsmux(opts = {}) {
47
+ if (opts.dryRun) return false;
48
+ try {
49
+ childProcess.execFileSync("psmux", ["-V"], { encoding: "utf8", stdio: "pipe" });
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ export function createDemoSession(sessionName, opts = {}) {
57
+ if (opts.dryRun) {
58
+ console.log(`[dry-run] psmux new-session -d -s ${sessionName}`);
59
+ console.log(`[dry-run] psmux split-window -h -t ${sessionName}`);
60
+ console.log(`[dry-run] psmux split-window -h -t ${sessionName}`);
61
+ return;
62
+ }
63
+ childProcess.execFileSync("psmux", ["new-session", "-d", "-s", sessionName], { stdio: "pipe" });
64
+ childProcess.execFileSync("psmux", ["split-window", "-h", "-t", sessionName], { stdio: "pipe" });
65
+ childProcess.execFileSync("psmux", ["split-window", "-h", "-t", sessionName], { stdio: "pipe" });
66
+ }
67
+
68
+ export function simulateWorker(pane, agentName, messages, opts = {}) {
69
+ const sessionName = opts.sessionName || SESSION_NAME;
70
+ for (const msg of messages) {
71
+ const escapedMsg = msg.replace(/'/g, "'\\''");
72
+ if (opts.dryRun) {
73
+ console.log(`[dry-run] psmux send-keys -t ${sessionName}:0.${pane} "echo '${escapedMsg}'" Enter`);
74
+ } else {
75
+ childProcess.execFileSync(
76
+ "psmux",
77
+ ["send-keys", "-t", `${sessionName}:0.${pane}`, `echo '${escapedMsg}'`, "Enter"],
78
+ { stdio: "pipe" },
79
+ );
80
+ }
81
+ }
82
+ }
83
+
84
+ export function showSummary() {
85
+ const lines = [
86
+ "",
87
+ "=== triflux demo summary ===",
88
+ " codex → JWT auth refactor [done]",
89
+ " gemini → UI render optimize [done]",
90
+ " claude → Security audit [done]",
91
+ "============================",
92
+ "",
93
+ ];
94
+ for (const line of lines) {
95
+ console.log(line);
96
+ }
97
+ }
98
+
99
+ export function cleanup(sessionName, opts = {}) {
100
+ if (opts.dryRun) {
101
+ console.log(`[dry-run] psmux kill-session -t ${sessionName}`);
102
+ return;
103
+ }
104
+ try {
105
+ childProcess.execFileSync("psmux", ["kill-session", "-t", sessionName], { stdio: "pipe" });
106
+ } catch {
107
+ // session may already be gone
108
+ }
109
+ }
110
+
111
+ async function wait(ms) {
112
+ return new Promise((resolve) => setTimeout(resolve, ms));
113
+ }
114
+
115
+ async function main() {
116
+ const { values: flags } = parseArgs({
117
+ options: {
118
+ "dry-run": { type: "boolean", default: false },
119
+ keep: { type: "boolean", default: false },
120
+ },
121
+ strict: false,
122
+ });
123
+
124
+ const psmuxAvailable = checkPsmux({ dryRun: flags["dry-run"] });
125
+ const dryRun = flags["dry-run"] || !psmuxAvailable;
126
+
127
+ if (!psmuxAvailable && !flags["dry-run"]) {
128
+ console.log("[demo] psmux not found — switching to dry-run mode");
129
+ }
130
+
131
+ const opts = {
132
+ dryRun,
133
+ keep: flags.keep,
134
+ sessionName: SESSION_NAME,
135
+ };
136
+
137
+ createDemoSession(SESSION_NAME, opts);
138
+
139
+ for (const { pane, agent, messages } of WORKERS) {
140
+ simulateWorker(pane, agent, messages, opts);
141
+ }
142
+
143
+ if (!opts.dryRun) {
144
+ await wait(2000);
145
+ }
146
+
147
+ showSummary();
148
+
149
+ if (!opts.keep) {
150
+ cleanup(SESSION_NAME, opts);
151
+ }
152
+ }
153
+
154
+ // Only run main when executed directly (not imported as a module)
155
+ // Normalize both paths to forward-slash for cross-platform comparison
156
+ function isDirectExec() {
157
+ if (!process.argv[1]) return false;
158
+ const scriptPath = new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
159
+ const argv1 = process.argv[1].replace(/\\/g, "/");
160
+ const norm = scriptPath.replace(/\\/g, "/");
161
+ return argv1 === norm || argv1.endsWith(norm);
162
+ }
163
+
164
+ if (isDirectExec()) {
165
+ main().catch((err) => {
166
+ console.error("demo error:", err.message);
167
+ process.exit(1);
168
+ });
169
+ }
@@ -202,8 +202,12 @@ async function main() {
202
202
  // codex/gemini 직접 CLI 호출 → deny (인라인 TFX_ALLOW_DIRECT_CLI=1 우회 허용)
203
203
  // 복합 명령(&&, ||, ;, |) 분리 후 각 세그먼트의 커맨드 위치만 검사 (args/quotes 안의 codex는 무시)
204
204
  // NOTE: || 는 | 보다 먼저 매칭되므로 logical OR이 단일 pipe로 잘못 분리되지 않음
205
+ // #37 Bug4: gh/git 명령은 본문에 codex/gemini 문자열이 있어도 차단하지 않음
206
+ const SAFE_CMD_RE = /^\s*(?:[\w_]+=\S+\s+)*\s*(gh|git)\b/;
205
207
  const cmdParts = cmd.split(/\s*(?:&&|\|\||\||;)\s*/);
206
208
  let hasDirectCli = cmdParts.some(part => {
209
+ // gh/git 세그먼트는 건너뜀 (이슈 본문/커밋 메시지 내 codex/gemini 언급은 정상)
210
+ if (SAFE_CMD_RE.test(part)) return false;
207
211
  // 1단계: env var prefix 제거 (FOO=bar ...)
208
212
  // 2단계: wrapper prefix 제거 (env, command, nohup, timeout N, 절대경로, bash -c/-lc "...")
209
213
  const stripped = part
@@ -216,11 +220,19 @@ async function main() {
216
220
  });
217
221
  // 2차 휴리스틱: 1차 세그먼트 검사를 통과한 간접 실행 패턴 탐지
218
222
  // full AST 파서 대신 현실적 위협 벡터만 커버 — eval, subshell, variable 확장
223
+ // 2차 휴리스틱: 간접 실행 패턴 탐지 (eval, subshell, variable 확장)
219
224
  if (!hasDirectCli) {
220
- hasDirectCli = (
221
- /\beval\b.*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd) ||
222
- /\$[({].*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd)
223
- );
225
+ const isAllSafeCmd = cmdParts.every(p => SAFE_CMD_RE.test(p));
226
+ if (isAllSafeCmd) {
227
+ // gh/git 전용: $(codex exec ...) 직접 명령 치환만 차단
228
+ // $(cat <<'EOF'\n...codex exec text...\nEOF) 같은 heredoc 텍스트는 허용
229
+ hasDirectCli = /\$\(\s*(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd);
230
+ } else {
231
+ hasDirectCli = (
232
+ /\beval\b.*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd) ||
233
+ /\$[({].*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd)
234
+ );
235
+ }
224
236
  }
225
237
 
226
238
  if (hasDirectCli) {