triflux 3.2.0-dev.1 → 3.2.0-dev.11

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 (53) hide show
  1. package/README.ko.md +26 -18
  2. package/README.md +26 -18
  3. package/bin/triflux.mjs +1614 -1084
  4. package/hooks/hooks.json +12 -0
  5. package/hooks/keyword-rules.json +354 -0
  6. package/hub/bridge.mjs +371 -193
  7. package/hub/hitl.mjs +45 -31
  8. package/hub/pipe.mjs +457 -0
  9. package/hub/router.mjs +422 -161
  10. package/hub/server.mjs +429 -344
  11. package/hub/store.mjs +388 -314
  12. package/hub/team/cli-team-common.mjs +348 -0
  13. package/hub/team/cli-team-control.mjs +393 -0
  14. package/hub/team/cli-team-start.mjs +516 -0
  15. package/hub/team/cli-team-status.mjs +269 -0
  16. package/hub/team/cli.mjs +99 -368
  17. package/hub/team/dashboard.mjs +165 -64
  18. package/hub/team/native-supervisor.mjs +300 -0
  19. package/hub/team/native.mjs +62 -0
  20. package/hub/team/nativeProxy.mjs +534 -0
  21. package/hub/team/orchestrator.mjs +90 -31
  22. package/hub/team/pane.mjs +149 -101
  23. package/hub/team/psmux.mjs +297 -0
  24. package/hub/team/session.mjs +608 -186
  25. package/hub/team/shared.mjs +13 -0
  26. package/hub/team/staleState.mjs +299 -0
  27. package/hub/tools.mjs +140 -53
  28. package/hub/workers/claude-worker.mjs +446 -0
  29. package/hub/workers/codex-mcp.mjs +414 -0
  30. package/hub/workers/factory.mjs +18 -0
  31. package/hub/workers/gemini-worker.mjs +349 -0
  32. package/hub/workers/interface.mjs +41 -0
  33. package/hud/hud-qos-status.mjs +1789 -1732
  34. package/package.json +6 -2
  35. package/scripts/__tests__/keyword-detector.test.mjs +234 -0
  36. package/scripts/hub-ensure.mjs +83 -0
  37. package/scripts/keyword-detector.mjs +272 -0
  38. package/scripts/keyword-rules-expander.mjs +521 -0
  39. package/scripts/lib/keyword-rules.mjs +168 -0
  40. package/scripts/psmux-steering-prototype.sh +368 -0
  41. package/scripts/run.cjs +62 -0
  42. package/scripts/setup.mjs +189 -7
  43. package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
  44. package/scripts/tfx-route-worker.mjs +161 -0
  45. package/scripts/tfx-route.sh +943 -508
  46. package/skills/tfx-auto/SKILL.md +90 -564
  47. package/skills/tfx-auto-codex/SKILL.md +77 -0
  48. package/skills/tfx-codex/SKILL.md +1 -4
  49. package/skills/tfx-doctor/SKILL.md +1 -0
  50. package/skills/tfx-gemini/SKILL.md +1 -4
  51. package/skills/tfx-multi/SKILL.md +296 -0
  52. package/skills/tfx-setup/SKILL.md +1 -4
  53. package/skills/tfx-team/SKILL.md +0 -172
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env bash
2
+ # psmux-steering-prototype.sh
3
+ # Windows psmux 환경에서 lead/codex-worker/gemini-worker pane을 만들고
4
+ # send-keys + pipe-pane 기반으로 실시간 CLI 스티어링을 실험하는 프로토타입.
5
+
6
+ set -euo pipefail
7
+
8
+ PSMUX_BIN="${PSMUX_BIN:-psmux}"
9
+ SESSION_NAME="${PSMUX_SESSION_NAME:-triflux-steering}"
10
+ WINDOW_NAME="${PSMUX_WINDOW_NAME:-control}"
11
+ PANE_LEAD="lead"
12
+ PANE_CODEX="codex-worker"
13
+ PANE_GEMINI="gemini-worker"
14
+ SHELL_COMMAND="${PSMUX_SHELL_COMMAND:-powershell.exe -NoLogo}"
15
+ CAPTURE_ROOT="${PSMUX_CAPTURE_ROOT:-${TMPDIR:-/tmp}/psmux-steering}"
16
+ CAPTURE_DIR="${CAPTURE_ROOT}/${SESSION_NAME}"
17
+ CAPTURE_HELPER_PATH="${CAPTURE_ROOT}/pipe-pane-capture.ps1"
18
+ COMPLETION_PREFIX="__TRIFLUX_DONE__:"
19
+ POLL_INTERVAL_SEC="${PSMUX_POLL_INTERVAL_SEC:-1}"
20
+
21
+ usage() {
22
+ cat <<'EOF'
23
+ Usage:
24
+ scripts/psmux-steering-prototype.sh start
25
+ scripts/psmux-steering-prototype.sh demo
26
+ scripts/psmux-steering-prototype.sh attach
27
+ scripts/psmux-steering-prototype.sh send <pane-name> <command text>
28
+ scripts/psmux-steering-prototype.sh send-no-enter <pane-name> <text>
29
+ scripts/psmux-steering-prototype.sh steer-ps <pane-name> <powershell command>
30
+ scripts/psmux-steering-prototype.sh wait <pane-name> <regex> [timeout-sec]
31
+ scripts/psmux-steering-prototype.sh logs
32
+ scripts/psmux-steering-prototype.sh cleanup
33
+
34
+ Pane names:
35
+ lead | codex-worker | gemini-worker
36
+
37
+ Environment overrides:
38
+ PSMUX_BIN
39
+ PSMUX_SESSION_NAME
40
+ PSMUX_WINDOW_NAME
41
+ PSMUX_SHELL_COMMAND
42
+ PSMUX_CAPTURE_ROOT
43
+ PSMUX_POLL_INTERVAL_SEC
44
+ EOF
45
+ }
46
+
47
+ log() {
48
+ printf '[psmux-steering] %s\n' "$*"
49
+ }
50
+
51
+ die() {
52
+ printf '[psmux-steering] ERROR: %s\n' "$*" >&2
53
+ exit 1
54
+ }
55
+
56
+ require_psmux() {
57
+ command -v "$PSMUX_BIN" >/dev/null 2>&1 || die "Cannot find '$PSMUX_BIN' in PATH."
58
+ }
59
+
60
+ session_target() {
61
+ printf '%s:%s' "$SESSION_NAME" "$WINDOW_NAME"
62
+ }
63
+
64
+ pane_target_from_index() {
65
+ local pane_index="$1"
66
+ printf '%s.%s' "$(session_target)" "$pane_index"
67
+ }
68
+
69
+ log_file_for() {
70
+ local pane_name="$1"
71
+ printf '%s/%s.log' "$CAPTURE_DIR" "$pane_name"
72
+ }
73
+
74
+ to_windows_path() {
75
+ local path_value="$1"
76
+
77
+ if command -v cygpath >/dev/null 2>&1; then
78
+ cygpath -aw "$path_value"
79
+ return 0
80
+ fi
81
+
82
+ printf '%s\n' "$path_value"
83
+ }
84
+
85
+ ensure_capture_helper() {
86
+ mkdir -p "$CAPTURE_ROOT"
87
+
88
+ cat >"$CAPTURE_HELPER_PATH" <<'EOF'
89
+ param(
90
+ [Parameter(Mandatory = $true)][string]$Path
91
+ )
92
+
93
+ $parent = Split-Path -Parent $Path
94
+ if ($parent) {
95
+ New-Item -ItemType Directory -Force -Path $parent | Out-Null
96
+ }
97
+
98
+ $reader = [Console]::In
99
+ while (($line = $reader.ReadLine()) -ne $null) {
100
+ Add-Content -LiteralPath $Path -Value $line -Encoding utf8
101
+ }
102
+ EOF
103
+ }
104
+
105
+ session_exists() {
106
+ "$PSMUX_BIN" has-session -t "$SESSION_NAME" >/dev/null 2>&1
107
+ }
108
+
109
+ resolve_pane_target() {
110
+ local pane_name="$1"
111
+ local pane_index
112
+
113
+ pane_index="$("$PSMUX_BIN" list-panes -t "$(session_target)" -F '#{pane_index} #{pane_title}' \
114
+ | awk -v wanted="$pane_name" '$2 == wanted { print $1; exit }')"
115
+
116
+ [[ -n "$pane_index" ]] || return 1
117
+ pane_target_from_index "$pane_index"
118
+ }
119
+
120
+ require_pane_target() {
121
+ local pane_name="$1"
122
+ local pane_target
123
+
124
+ pane_target="$(resolve_pane_target "$pane_name")"
125
+ [[ -n "$pane_target" ]] || die "Pane '$pane_name' not found in session '$SESSION_NAME'."
126
+ printf '%s\n' "$pane_target"
127
+ }
128
+
129
+ set_pane_title() {
130
+ local pane_target="$1"
131
+ local pane_name="$2"
132
+
133
+ "$PSMUX_BIN" select-pane -t "$pane_target" -T "$pane_name" >/dev/null
134
+ }
135
+
136
+ start_capture_for_pane() {
137
+ local pane_name="$1"
138
+ local pane_target log_file helper_windows_path log_windows_path
139
+
140
+ pane_target="$(require_pane_target "$pane_name")"
141
+ log_file="$(log_file_for "$pane_name")"
142
+ ensure_capture_helper
143
+ helper_windows_path="$(to_windows_path "$CAPTURE_HELPER_PATH")"
144
+ log_windows_path="$(to_windows_path "$log_file")"
145
+
146
+ mkdir -p "$CAPTURE_DIR"
147
+ : >"$log_file"
148
+
149
+ "$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
150
+ "$PSMUX_BIN" pipe-pane -t "$pane_target" powershell.exe -NoLogo -NoProfile -File "$helper_windows_path" "$log_windows_path" >/dev/null
151
+ refresh_snapshot_for_pane "$pane_name"
152
+ }
153
+
154
+ start_capture_for_all_panes() {
155
+ start_capture_for_pane "$PANE_LEAD"
156
+ start_capture_for_pane "$PANE_CODEX"
157
+ start_capture_for_pane "$PANE_GEMINI"
158
+ }
159
+
160
+ stop_capture_for_pane() {
161
+ local pane_name="$1"
162
+ local pane_target
163
+
164
+ pane_target="$(resolve_pane_target "$pane_name" || true)"
165
+ [[ -n "$pane_target" ]] || return 0
166
+ "$PSMUX_BIN" pipe-pane -t "$pane_target" >/dev/null 2>&1 || true
167
+ }
168
+
169
+ refresh_snapshot_for_pane() {
170
+ local pane_name="$1"
171
+ local pane_target log_file
172
+
173
+ pane_target="$(require_pane_target "$pane_name")"
174
+ log_file="$(log_file_for "$pane_name")"
175
+ mkdir -p "$CAPTURE_DIR"
176
+
177
+ # Detached Windows sessions may not flush pipe-pane reliably yet.
178
+ # Overwriting the log with a fresh capture-pane snapshot keeps
179
+ # completion detection deterministic for the prototype.
180
+ "$PSMUX_BIN" capture-pane -t "$pane_target" -p >"$log_file"
181
+ }
182
+
183
+ send_keys_to_pane() {
184
+ local pane_name="$1"
185
+ local text="$2"
186
+ local submit="${3:-1}"
187
+ local pane_target
188
+
189
+ pane_target="$(require_pane_target "$pane_name")"
190
+ "$PSMUX_BIN" send-keys -t "$pane_target" -l "$text"
191
+ if [[ "$submit" != "0" ]]; then
192
+ "$PSMUX_BIN" send-keys -t "$pane_target" C-m
193
+ fi
194
+ }
195
+
196
+ dispatch_powershell_command() {
197
+ local pane_name="$1"
198
+ local command_text="$2"
199
+ local token wrapped
200
+
201
+ token="${pane_name}-$(date +%s)-$RANDOM"
202
+ wrapped="${command_text}; \$trifluxExit = if (\$null -ne \$LASTEXITCODE) { [int]\$LASTEXITCODE } else { 0 }; Write-Output \"${COMPLETION_PREFIX}${token}:\$trifluxExit\""
203
+
204
+ send_keys_to_pane "$pane_name" "$wrapped" 1
205
+ printf '%s\n' "$token"
206
+ }
207
+
208
+ wait_for_pattern() {
209
+ local pane_name="$1"
210
+ local pattern="$2"
211
+ local timeout_sec="${3:-300}"
212
+ local log_file deadline
213
+
214
+ log_file="$(log_file_for "$pane_name")"
215
+ [[ -f "$log_file" ]] || die "Log file for pane '$pane_name' does not exist. Start capture first."
216
+
217
+ deadline=$((SECONDS + timeout_sec))
218
+ while (( SECONDS <= deadline )); do
219
+ refresh_snapshot_for_pane "$pane_name"
220
+ if grep -Eq -- "$pattern" "$log_file"; then
221
+ return 0
222
+ fi
223
+ sleep "$POLL_INTERVAL_SEC"
224
+ done
225
+
226
+ return 1
227
+ }
228
+
229
+ wait_for_completion_token() {
230
+ local pane_name="$1"
231
+ local token="$2"
232
+ local timeout_sec="${3:-300}"
233
+ local pattern
234
+
235
+ pattern="${COMPLETION_PREFIX}${token}:[0-9]+"
236
+ wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"
237
+ }
238
+
239
+ print_log_locations() {
240
+ mkdir -p "$CAPTURE_DIR"
241
+ printf '%s\t%s\n' "$PANE_LEAD" "$(log_file_for "$PANE_LEAD")"
242
+ printf '%s\t%s\n' "$PANE_CODEX" "$(log_file_for "$PANE_CODEX")"
243
+ printf '%s\t%s\n' "$PANE_GEMINI" "$(log_file_for "$PANE_GEMINI")"
244
+ }
245
+
246
+ create_session_layout() {
247
+ local lead_index codex_index gemini_index
248
+
249
+ require_psmux
250
+
251
+ if session_exists; then
252
+ die "Session '$SESSION_NAME' already exists. Run cleanup first or set PSMUX_SESSION_NAME."
253
+ fi
254
+
255
+ mkdir -p "$CAPTURE_DIR"
256
+
257
+ lead_index="$("$PSMUX_BIN" new-session -d -P -F '#{pane_index}' -s "$SESSION_NAME" -n "$WINDOW_NAME" -- $SHELL_COMMAND)"
258
+ codex_index="$("$PSMUX_BIN" split-window -h -P -F '#{pane_index}' -t "$(session_target)" -- $SHELL_COMMAND)"
259
+ gemini_index="$("$PSMUX_BIN" split-window -v -P -F '#{pane_index}' -t "$(pane_target_from_index "$codex_index")" -- $SHELL_COMMAND)"
260
+
261
+ set_pane_title "$(pane_target_from_index "$lead_index")" "$PANE_LEAD"
262
+ set_pane_title "$(pane_target_from_index "$codex_index")" "$PANE_CODEX"
263
+ set_pane_title "$(pane_target_from_index "$gemini_index")" "$PANE_GEMINI"
264
+
265
+ "$PSMUX_BIN" select-layout -t "$(session_target)" tiled >/dev/null
266
+ "$PSMUX_BIN" select-pane -t "$(pane_target_from_index "$lead_index")" >/dev/null
267
+
268
+ start_capture_for_all_panes
269
+ }
270
+
271
+ show_start_summary() {
272
+ log "Session created: $SESSION_NAME"
273
+ log "Window: $WINDOW_NAME"
274
+ log "Attach with: $PSMUX_BIN attach -t $SESSION_NAME"
275
+ print_log_locations
276
+ }
277
+
278
+ run_demo() {
279
+ local lead_token codex_token gemini_token
280
+
281
+ create_session_layout
282
+
283
+ lead_token="$(dispatch_powershell_command "$PANE_LEAD" 'Write-Host "lead pane ready"')"
284
+ codex_token="$(dispatch_powershell_command "$PANE_CODEX" 'Write-Host "codex-worker pane ready"')"
285
+ gemini_token="$(dispatch_powershell_command "$PANE_GEMINI" 'Write-Host "gemini-worker pane ready"')"
286
+
287
+ wait_for_completion_token "$PANE_LEAD" "$lead_token" 30 || die "Lead pane demo command timed out."
288
+ wait_for_completion_token "$PANE_CODEX" "$codex_token" 30 || die "Codex pane demo command timed out."
289
+ wait_for_completion_token "$PANE_GEMINI" "$gemini_token" 30 || die "Gemini pane demo command timed out."
290
+
291
+ show_start_summary
292
+ }
293
+
294
+ cleanup() {
295
+ stop_capture_for_pane "$PANE_LEAD"
296
+ stop_capture_for_pane "$PANE_CODEX"
297
+ stop_capture_for_pane "$PANE_GEMINI"
298
+
299
+ if session_exists; then
300
+ "$PSMUX_BIN" kill-session -t "$SESSION_NAME" >/dev/null 2>&1 || true
301
+ fi
302
+ }
303
+
304
+ main() {
305
+ local action="${1:-demo}"
306
+
307
+ case "$action" in
308
+ start)
309
+ create_session_layout
310
+ show_start_summary
311
+ ;;
312
+ demo)
313
+ run_demo
314
+ ;;
315
+ attach)
316
+ require_psmux
317
+ "$PSMUX_BIN" attach -t "$SESSION_NAME"
318
+ ;;
319
+ send)
320
+ [[ $# -ge 3 ]] || die "Usage: $0 send <pane-name> <command text>"
321
+ shift
322
+ local pane_name="$1"
323
+ shift
324
+ send_keys_to_pane "$pane_name" "$*" 1
325
+ ;;
326
+ send-no-enter)
327
+ [[ $# -ge 3 ]] || die "Usage: $0 send-no-enter <pane-name> <text>"
328
+ shift
329
+ local pane_name="$1"
330
+ shift
331
+ send_keys_to_pane "$pane_name" "$*" 0
332
+ ;;
333
+ steer-ps)
334
+ [[ $# -ge 3 ]] || die "Usage: $0 steer-ps <pane-name> <powershell command>"
335
+ shift
336
+ local pane_name="$1"
337
+ shift
338
+ dispatch_powershell_command "$pane_name" "$*"
339
+ ;;
340
+ wait)
341
+ [[ $# -ge 3 ]] || die "Usage: $0 wait <pane-name> <regex> [timeout-sec]"
342
+ shift
343
+ local pane_name="$1"
344
+ local pattern="$2"
345
+ local timeout_sec="${3:-300}"
346
+ if wait_for_pattern "$pane_name" "$pattern" "$timeout_sec"; then
347
+ log "Matched pattern for pane '$pane_name': $pattern"
348
+ else
349
+ die "Timed out waiting for pane '$pane_name' pattern: $pattern"
350
+ fi
351
+ ;;
352
+ logs)
353
+ print_log_locations
354
+ ;;
355
+ cleanup)
356
+ cleanup
357
+ ;;
358
+ -h|--help|help)
359
+ usage
360
+ ;;
361
+ *)
362
+ usage
363
+ die "Unknown action: $action"
364
+ ;;
365
+ esac
366
+ }
367
+
368
+ main "$@"
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { execFileSync } = require("child_process");
5
+ const { existsSync, readFileSync } = require("fs");
6
+ const { dirname, isAbsolute, join, resolve } = require("path");
7
+
8
+ function resolvePluginRoot() {
9
+ if (process.env.CLAUDE_PLUGIN_ROOT) return process.env.CLAUDE_PLUGIN_ROOT;
10
+ return dirname(__dirname);
11
+ }
12
+
13
+ function resolveTargetPath(rawTarget) {
14
+ if (!rawTarget || typeof rawTarget !== "string") return null;
15
+
16
+ const pluginRoot = resolvePluginRoot();
17
+ const trimmed = rawTarget.trim();
18
+
19
+ if (trimmed.startsWith("${CLAUDE_PLUGIN_ROOT}/")) {
20
+ return join(pluginRoot, trimmed.replace("${CLAUDE_PLUGIN_ROOT}/", ""));
21
+ }
22
+
23
+ if (trimmed.startsWith("/scripts/")) {
24
+ return join(pluginRoot, trimmed.replace(/^\/+/, ""));
25
+ }
26
+
27
+ if (isAbsolute(trimmed)) return trimmed;
28
+ return resolve(process.cwd(), trimmed);
29
+ }
30
+
31
+ const targetArg = process.argv[2];
32
+ if (!targetArg) {
33
+ process.exit(0);
34
+ }
35
+
36
+ const targetPath = resolveTargetPath(targetArg);
37
+ if (!targetPath || !existsSync(targetPath)) {
38
+ process.exit(0);
39
+ }
40
+
41
+ const stdinBuffer = (() => {
42
+ try {
43
+ return readFileSync(0);
44
+ } catch {
45
+ return Buffer.alloc(0);
46
+ }
47
+ })();
48
+
49
+ try {
50
+ execFileSync(process.execPath, [targetPath, ...process.argv.slice(3)], {
51
+ env: process.env,
52
+ stdio: ["pipe", "inherit", "inherit"],
53
+ input: stdinBuffer,
54
+ windowsHide: true
55
+ });
56
+ process.exit(0);
57
+ } catch (error) {
58
+ if (typeof error?.status === "number") {
59
+ process.exit(error.status);
60
+ }
61
+ process.exit(0);
62
+ }
package/scripts/setup.mjs CHANGED
@@ -7,9 +7,30 @@
7
7
  import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
8
8
  import { join, dirname } from "path";
9
9
  import { homedir } from "os";
10
+ import { spawn } from "child_process";
11
+ import { fileURLToPath } from "url";
10
12
 
11
- const PLUGIN_ROOT = dirname(dirname(new URL(import.meta.url).pathname)).replace(/^\/([A-Z]:)/, "$1");
13
+ const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
12
14
  const CLAUDE_DIR = join(homedir(), ".claude");
15
+ const CODEX_DIR = join(homedir(), ".codex");
16
+ const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
17
+
18
+ const REQUIRED_CODEX_PROFILES = [
19
+ {
20
+ name: "xhigh",
21
+ lines: [
22
+ 'model = "gpt-5.3-codex"',
23
+ 'model_reasoning_effort = "xhigh"',
24
+ ],
25
+ },
26
+ {
27
+ name: "spark_fast",
28
+ lines: [
29
+ 'model = "gpt-5.1-codex-mini"',
30
+ 'model_reasoning_effort = "low"',
31
+ ],
32
+ },
33
+ ];
13
34
 
14
35
  // ── 파일 동기화 ──
15
36
 
@@ -24,6 +45,36 @@ const SYNC_MAP = [
24
45
  dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
25
46
  label: "tfx-route-post.mjs",
26
47
  },
48
+ {
49
+ src: join(PLUGIN_ROOT, "scripts", "tfx-route-worker.mjs"),
50
+ dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
51
+ label: "tfx-route-worker.mjs",
52
+ },
53
+ {
54
+ src: join(PLUGIN_ROOT, "hub", "workers", "codex-mcp.mjs"),
55
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "codex-mcp.mjs"),
56
+ label: "hub/workers/codex-mcp.mjs",
57
+ },
58
+ {
59
+ src: join(PLUGIN_ROOT, "hub", "workers", "interface.mjs"),
60
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "interface.mjs"),
61
+ label: "hub/workers/interface.mjs",
62
+ },
63
+ {
64
+ src: join(PLUGIN_ROOT, "hub", "workers", "gemini-worker.mjs"),
65
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "gemini-worker.mjs"),
66
+ label: "hub/workers/gemini-worker.mjs",
67
+ },
68
+ {
69
+ src: join(PLUGIN_ROOT, "hub", "workers", "claude-worker.mjs"),
70
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
71
+ label: "hub/workers/claude-worker.mjs",
72
+ },
73
+ {
74
+ src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
75
+ dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
76
+ label: "hub/workers/factory.mjs",
77
+ },
27
78
  {
28
79
  src: join(PLUGIN_ROOT, "hud", "hud-qos-status.mjs"),
29
80
  dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
@@ -41,6 +92,54 @@ function getVersion(filePath) {
41
92
  }
42
93
  }
43
94
 
95
+ function shouldSyncTextFile(src, dst) {
96
+ if (!existsSync(dst)) return true;
97
+ try {
98
+ return readFileSync(src, "utf8") !== readFileSync(dst, "utf8");
99
+ } catch {
100
+ return true;
101
+ }
102
+ }
103
+
104
+ function escapeRegExp(value) {
105
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
106
+ }
107
+
108
+ function hasProfileSection(tomlContent, profileName) {
109
+ const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
110
+ return new RegExp(section, "m").test(tomlContent);
111
+ }
112
+
113
+ function ensureCodexProfiles() {
114
+ try {
115
+ if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
116
+
117
+ const original = existsSync(CODEX_CONFIG_PATH)
118
+ ? readFileSync(CODEX_CONFIG_PATH, "utf8")
119
+ : "";
120
+
121
+ let updated = original;
122
+ let added = 0;
123
+
124
+ for (const profile of REQUIRED_CODEX_PROFILES) {
125
+ if (hasProfileSection(updated, profile.name)) continue;
126
+
127
+ if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
128
+ if (updated.trim().length > 0) updated += "\n";
129
+ updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
130
+ added++;
131
+ }
132
+
133
+ if (added > 0) {
134
+ writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
135
+ }
136
+
137
+ return added;
138
+ } catch {
139
+ return 0;
140
+ }
141
+ }
142
+
44
143
  let synced = 0;
45
144
 
46
145
  for (const { src, dst, label } of SYNC_MAP) {
@@ -56,9 +155,7 @@ for (const { src, dst, label } of SYNC_MAP) {
56
155
  try { chmodSync(dst, 0o755); } catch {}
57
156
  synced++;
58
157
  } else {
59
- const srcVersion = getVersion(src);
60
- const dstVersion = getVersion(dst);
61
- if (srcVersion && dstVersion && srcVersion !== dstVersion) {
158
+ if (shouldSyncTextFile(src, dst)) {
62
159
  copyFileSync(src, dst);
63
160
  try { chmodSync(dst, 0o755); } catch {}
64
161
  synced++;
@@ -66,6 +163,34 @@ for (const { src, dst, label } of SYNC_MAP) {
66
163
  }
67
164
  }
68
165
 
166
+ // ── Worker 의존성 동기화 (MCP SDK + transitive deps) ──
167
+
168
+ const workerNodeModules = join(CLAUDE_DIR, "scripts", "node_modules");
169
+ const mcpSdkPath = join(workerNodeModules, "@modelcontextprotocol", "sdk");
170
+ const srcNodeModules = join(PLUGIN_ROOT, "node_modules");
171
+
172
+ // native 모듈은 제외 (플랫폼 의존적, worker에서 불필요)
173
+ const SKIP_PACKAGES = new Set(["better-sqlite3", "prebuild-install", "node-abi", "node-addon-api"]);
174
+
175
+ if (!existsSync(mcpSdkPath) && existsSync(srcNodeModules)) {
176
+ try {
177
+ const { cpSync } = await import("fs");
178
+ for (const entry of readdirSync(srcNodeModules)) {
179
+ if (SKIP_PACKAGES.has(entry)) continue;
180
+
181
+ const src = join(srcNodeModules, entry);
182
+ const dst = join(workerNodeModules, entry);
183
+ if (existsSync(dst)) continue;
184
+
185
+ mkdirSync(dirname(dst), { recursive: true });
186
+ cpSync(src, dst, { recursive: true });
187
+ }
188
+ synced++;
189
+ } catch {
190
+ // best effort: 의존성 복사 실패 시 exec fallback으로 동작
191
+ }
192
+ }
193
+
69
194
  // ── 스킬 동기화 ──
70
195
 
71
196
  const skillsSrc = join(PLUGIN_ROOT, "skills");
@@ -130,6 +255,36 @@ if (existsSync(hudPath)) {
130
255
  }
131
256
  }
132
257
 
258
+ // ── Agent Teams 환경변수 자동 설정 ──
259
+
260
+ try {
261
+ let agentSettings = {};
262
+ if (existsSync(settingsPath)) {
263
+ agentSettings = JSON.parse(readFileSync(settingsPath, "utf8"));
264
+ }
265
+
266
+ if (!agentSettings.env) agentSettings.env = {};
267
+ let agentSettingsChanged = false;
268
+
269
+ if (agentSettings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
270
+ agentSettings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
271
+ agentSettingsChanged = true;
272
+ }
273
+
274
+ // teammateMode: auto (tmux 밖이면 in-process, 안이면 split-pane)
275
+ if (!agentSettings.teammateMode) {
276
+ agentSettings.teammateMode = "auto";
277
+ agentSettingsChanged = true;
278
+ }
279
+
280
+ if (agentSettingsChanged) {
281
+ writeFileSync(settingsPath, JSON.stringify(agentSettings, null, 2) + "\n", "utf8");
282
+ synced++;
283
+ }
284
+ } catch {
285
+ // settings.json 파싱 실패 시 무시 — 기존 설정 보존
286
+ }
287
+
133
288
  // ── Stale PID 파일 정리 (hub 좀비 방지) ──
134
289
 
135
290
  const HUB_PID_FILE = join(CLAUDE_DIR, "cache", "tfx-hub", "hub.pid");
@@ -196,9 +351,14 @@ if (process.platform === "win32") {
196
351
  }
197
352
  }
198
353
 
199
- // ── MCP 인벤토리 백그라운드 갱신 ──
354
+ // ── Codex 프로필 자동 보정 ──
200
355
 
201
- import { spawn } from "child_process";
356
+ const codexProfilesAdded = ensureCodexProfiles();
357
+ if (codexProfilesAdded > 0) {
358
+ synced++;
359
+ }
360
+
361
+ // ── MCP 인벤토리 백그라운드 갱신 ──
202
362
 
203
363
  const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
204
364
  if (existsSync(mcpCheck)) {
@@ -209,6 +369,26 @@ if (existsSync(mcpCheck)) {
209
369
  child.unref(); // 부모 프로세스와 분리 — 비동기 실행
210
370
  }
211
371
 
372
+ // ── Hub 헬스체크 + 자동 기동 (세션 시작 백그라운드) ──
373
+ // setup 훅이 포그라운드 지연을 만들지 않도록 별도 detached 프로세스로 처리한다.
374
+ const hubEnsure = join(PLUGIN_ROOT, "scripts", "hub-ensure.mjs");
375
+ const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
376
+ const isCi = /^(1|true)$/i.test(process.env.CI || "");
377
+ const disableHubAutostart = process.env.TFX_DISABLE_HUB_AUTOSTART === "1";
378
+
379
+ if (!isPostinstall && !isCi && !disableHubAutostart && existsSync(hubEnsure)) {
380
+ try {
381
+ const child = spawn(process.execPath, [hubEnsure], {
382
+ env: process.env,
383
+ detached: true,
384
+ stdio: "ignore",
385
+ });
386
+ child.unref();
387
+ } catch {
388
+ // best effort: 실패해도 setup 흐름은 지속
389
+ }
390
+ }
391
+
212
392
  // ── postinstall 배너 (npm install 시에만 출력) ──
213
393
 
214
394
  if (process.env.npm_lifecycle_event === "postinstall") {
@@ -239,7 +419,8 @@ ${B}Commands:${R}
239
419
  ${C}triflux${R} setup 파일 동기화 + HUD 설정
240
420
  ${C}triflux${R} doctor CLI 진단 (Codex/Gemini 확인)
241
421
  ${C}triflux${R} list 설치된 스킬 목록
242
- ${C}triflux${R} update 최신 버전으로 업데이트
422
+ ${C}triflux${R} update 최신 안정 버전으로 업데이트
423
+ ${C}triflux${R} update --dev dev 채널로 업데이트 (${D}dev 별칭 지원${R})
243
424
 
244
425
  ${B}Shortcuts:${R}
245
426
  ${C}tfx${R} triflux 축약
@@ -248,6 +429,7 @@ ${B}Shortcuts:${R}
248
429
 
249
430
  ${B}Skills (Claude Code):${R}
250
431
  ${C}/tfx-auto${R} "작업" 자동 분류 + 병렬 실행
432
+ ${C}/tfx-auto-codex${R} "작업" Codex 리드 + Gemini 유지
251
433
  ${C}/tfx-codex${R} "작업" Codex 전용 모드
252
434
  ${C}/tfx-gemini${R} "작업" Gemini 전용 모드
253
435
  ${C}/tfx-setup${R} HUD 설정 + 진단