triflux 10.9.21 → 10.9.22

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 (99) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/config/mcp-registry.json +29 -0
  4. package/hub/account-broker.mjs +6 -4
  5. package/hub/cli-adapter-base.mjs +14 -14
  6. package/hub/lib/env-detect.mjs +47 -20
  7. package/hub/server.mjs +17 -15
  8. package/hub/team/headless.mjs +10 -0
  9. package/hub/team/swarm-hypervisor.mjs +2 -2
  10. package/hud/constants.mjs +24 -13
  11. package/hud/renderers.mjs +2 -1
  12. package/package.json +62 -21
  13. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  14. package/scripts/__tests__/release-governance.test.mjs +148 -0
  15. package/scripts/doctor-diagnose.mjs +6 -7
  16. package/scripts/lib/cross-review-utils.mjs +2 -2
  17. package/scripts/lib/mcp-filter.mjs +9 -5
  18. package/scripts/release/bump-version.mjs +77 -0
  19. package/scripts/release/check-sync.mjs +51 -0
  20. package/scripts/release/lib.mjs +303 -0
  21. package/scripts/release/prepare.mjs +85 -0
  22. package/scripts/release/publish.mjs +87 -0
  23. package/scripts/release/verify.mjs +81 -0
  24. package/scripts/release/version-manifest.json +26 -0
  25. package/scripts/remote-spawn.mjs +3 -3
  26. package/scripts/setup.mjs +18 -15
  27. package/scripts/tfx-route.sh +64 -8
  28. package/tui/codex-profile.mjs +457 -0
  29. package/tui/core.mjs +266 -0
  30. package/tui/doctor.mjs +375 -0
  31. package/tui/gemini-profile.mjs +299 -0
  32. package/tui/monitor-data.mjs +152 -0
  33. package/tui/monitor.mjs +339 -0
  34. package/tui/setup.mjs +598 -0
  35. package/CLAUDE.md +0 -212
  36. package/references/hosts.json +0 -46
  37. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  38. package/skills/tfx-workspace/evals/evals.json +0 -79
  39. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  40. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  43. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  47. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  50. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  54. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  57. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  61. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  64. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  68. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  69. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  72. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  76. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  79. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  83. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  84. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  87. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  91. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  92. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  93. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  94. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  95. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  96. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  97. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  98. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  99. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ assertVersionSync,
7
+ buildReleaseNotes,
8
+ ensureGitClean,
9
+ parseArgs,
10
+ ROOT,
11
+ runCommand,
12
+ } from "./lib.mjs";
13
+
14
+ export async function prepareRelease({
15
+ version,
16
+ rootDir = ROOT,
17
+ allowDirty = false,
18
+ dryRun = true,
19
+ execFileSyncFn,
20
+ } = {}) {
21
+ const sync = assertVersionSync({ rootDir });
22
+ if (!sync.ok) {
23
+ throw new Error(
24
+ "Version sync failed. Run scripts/release/check-sync.mjs first.",
25
+ );
26
+ }
27
+
28
+ const gitState = ensureGitClean({ rootDir, execFileSyncFn });
29
+ if (!gitState.clean && !allowDirty) {
30
+ throw new Error(
31
+ "Working tree is dirty. Re-run with --allow-dirty only for scaffolding.",
32
+ );
33
+ }
34
+
35
+ const releaseVersion = version || sync.rootVersion;
36
+ const commands = [
37
+ ["npm", ["test"]],
38
+ ["npm", ["run", "lint"]],
39
+ ["npm", ["pack", "--dry-run"]],
40
+ ];
41
+
42
+ if (!dryRun) {
43
+ for (const [command, args] of commands) {
44
+ runCommand(command, args, { cwd: rootDir, execFileSyncFn });
45
+ }
46
+ }
47
+
48
+ const notes = buildReleaseNotes({
49
+ version: releaseVersion,
50
+ rootDir,
51
+ execFileSyncFn,
52
+ });
53
+ const notesPath = join(
54
+ rootDir,
55
+ ".omx",
56
+ "plans",
57
+ `release-notes-v${releaseVersion}.md`,
58
+ );
59
+ writeFileSync(notesPath, notes, "utf8");
60
+
61
+ return {
62
+ ok: true,
63
+ version: releaseVersion,
64
+ clean: gitState.clean,
65
+ allowDirty,
66
+ dryRun,
67
+ commands: commands.map(([command, args]) => [command, ...args].join(" ")),
68
+ releaseNotesPath: notesPath,
69
+ };
70
+ }
71
+
72
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
73
+ const args = parseArgs(process.argv.slice(2));
74
+ const result = await prepareRelease({
75
+ version: args.version,
76
+ rootDir: args.root,
77
+ allowDirty: Boolean(args["allow-dirty"]),
78
+ dryRun: !args.execute,
79
+ });
80
+ if (args.json) {
81
+ console.log(JSON.stringify(result, null, 2));
82
+ } else {
83
+ console.log(JSON.stringify(result, null, 2));
84
+ }
85
+ }
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { assertVersionSync, parseArgs, ROOT, runCommand } from "./lib.mjs";
5
+
6
+ export async function publishRelease({
7
+ version,
8
+ rootDir = ROOT,
9
+ channel = "stable",
10
+ dryRun = true,
11
+ createGithubRelease = true,
12
+ execFileSyncFn,
13
+ } = {}) {
14
+ const sync = assertVersionSync({ rootDir });
15
+ if (!sync.ok) {
16
+ throw new Error("Version sync failed. Refusing to publish.");
17
+ }
18
+
19
+ const releaseVersion = version || sync.rootVersion;
20
+ const npmTag = channel === "canary" ? "canary" : "latest";
21
+ const notesPath = join(
22
+ rootDir,
23
+ ".omx",
24
+ "plans",
25
+ `release-notes-v${releaseVersion}.md`,
26
+ );
27
+ const steps = [
28
+ {
29
+ label: "npm publish",
30
+ command: "npm",
31
+ args: ["publish", "--tag", npmTag],
32
+ },
33
+ { label: "git tag", command: "git", args: ["tag", `v${releaseVersion}`] },
34
+ {
35
+ label: "git push",
36
+ command: "git",
37
+ args: ["push", "origin", "HEAD", "--tags"],
38
+ },
39
+ ];
40
+
41
+ if (createGithubRelease) {
42
+ steps.push({
43
+ label: "gh release create",
44
+ command: "gh",
45
+ args: [
46
+ "release",
47
+ "create",
48
+ `v${releaseVersion}`,
49
+ "--title",
50
+ `v${releaseVersion}`,
51
+ "--notes-file",
52
+ notesPath,
53
+ ],
54
+ });
55
+ }
56
+
57
+ if (!dryRun) {
58
+ for (const step of steps) {
59
+ runCommand(step.command, step.args, { cwd: rootDir, execFileSyncFn });
60
+ }
61
+ }
62
+
63
+ return {
64
+ ok: true,
65
+ version: releaseVersion,
66
+ channel,
67
+ npmTag,
68
+ dryRun,
69
+ notesPath,
70
+ steps: steps.map((step) => ({
71
+ label: step.label,
72
+ command: [step.command, ...step.args].join(" "),
73
+ })),
74
+ };
75
+ }
76
+
77
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
78
+ const args = parseArgs(process.argv.slice(2));
79
+ const result = await publishRelease({
80
+ version: args.version,
81
+ rootDir: args.root,
82
+ channel: args.channel || "stable",
83
+ dryRun: !args.execute,
84
+ createGithubRelease: !args["skip-gh-release"],
85
+ });
86
+ console.log(JSON.stringify(result, null, 2));
87
+ }
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { assertVersionSync, parseArgs, ROOT } from "./lib.mjs";
5
+
6
+ export async function verifyRelease({
7
+ version,
8
+ rootDir = ROOT,
9
+ dryRun = true,
10
+ execFileSyncFn = execFileSync,
11
+ } = {}) {
12
+ const sync = assertVersionSync({ rootDir });
13
+ if (!sync.ok) {
14
+ throw new Error("Version sync failed. Fix metadata before verify.");
15
+ }
16
+ const releaseVersion = version || sync.rootVersion;
17
+ const checks = [
18
+ {
19
+ name: "version-sync",
20
+ ok: true,
21
+ detail: `repo metadata matches ${releaseVersion}`,
22
+ },
23
+ ];
24
+
25
+ if (!dryRun) {
26
+ const npmVersion = execFileSyncFn("npm", ["view", "triflux", "version"], {
27
+ cwd: rootDir,
28
+ encoding: "utf8",
29
+ }).trim();
30
+ checks.push({
31
+ name: "npm-view",
32
+ ok: npmVersion === releaseVersion,
33
+ detail: npmVersion,
34
+ });
35
+
36
+ const ghRelease = execFileSyncFn(
37
+ "gh",
38
+ ["release", "view", `v${releaseVersion}`, "--json", "tagName"],
39
+ {
40
+ cwd: rootDir,
41
+ encoding: "utf8",
42
+ },
43
+ ).trim();
44
+ checks.push({
45
+ name: "github-release",
46
+ ok: ghRelease.length > 0,
47
+ detail: ghRelease,
48
+ });
49
+ } else {
50
+ checks.push(
51
+ {
52
+ name: "npm-view",
53
+ ok: null,
54
+ detail: `would run: npm view triflux version`,
55
+ },
56
+ {
57
+ name: "github-release",
58
+ ok: null,
59
+ detail: `would run: gh release view v${releaseVersion} --json tagName`,
60
+ },
61
+ );
62
+ }
63
+
64
+ return {
65
+ ok: checks.every((check) => check.ok !== false),
66
+ version: releaseVersion,
67
+ dryRun,
68
+ checks,
69
+ };
70
+ }
71
+
72
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
73
+ const args = parseArgs(process.argv.slice(2));
74
+ const result = await verifyRelease({
75
+ version: args.version,
76
+ rootDir: args.root,
77
+ dryRun: !args.execute,
78
+ });
79
+ console.log(JSON.stringify(result, null, 2));
80
+ process.exitCode = result.ok ? 0 : 1;
81
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "canonicalFile": "package.json",
3
+ "canonicalPath": ["version"],
4
+ "targets": [
5
+ {
6
+ "file": "package.json",
7
+ "paths": [["version"]]
8
+ },
9
+ {
10
+ "file": "packages/triflux/package.json",
11
+ "paths": [["version"]]
12
+ },
13
+ {
14
+ "file": ".claude-plugin/plugin.json",
15
+ "paths": [["version"]]
16
+ },
17
+ {
18
+ "file": ".claude-plugin/marketplace.json",
19
+ "paths": [["version"], ["plugins", 0, "version"]]
20
+ },
21
+ {
22
+ "file": "package-lock.json",
23
+ "paths": [["version"], ["packages", "", "version"]]
24
+ }
25
+ ]
26
+ }
@@ -591,7 +591,7 @@ function rewritePromptPaths(prompt, stagedFiles) {
591
591
  return rewritten;
592
592
  }
593
593
 
594
- function spawnLocalFallback(args, claudePath, prompt) {
594
+ async function spawnLocalFallback(args, claudePath, prompt) {
595
595
  const dir = args.dir ? resolve(args.dir) : process.cwd();
596
596
 
597
597
  if (!IS_WINDOWS_LOCAL) {
@@ -626,7 +626,7 @@ function spawnLocalFallback(args, claudePath, prompt) {
626
626
  }
627
627
  }
628
628
 
629
- function spawnRemoteFallback(args, promptContext) {
629
+ async function spawnRemoteFallback(args, promptContext) {
630
630
  const { host } = args;
631
631
  if (!host) {
632
632
  console.error("--host required for remote spawn");
@@ -999,7 +999,7 @@ function listSpawnSessions() {
999
999
  }
1000
1000
  }
1001
1001
 
1002
- function openAttachTab(sessionName, title = null) {
1002
+ async function openAttachTab(sessionName, title = null) {
1003
1003
  if (IS_WINDOWS_LOCAL) {
1004
1004
  const wtArgs = title
1005
1005
  try {
package/scripts/setup.mjs CHANGED
@@ -64,6 +64,23 @@ const REQUIRED_CODEX_PROFILES = [
64
64
 
65
65
  const HUD_SYNC_EXCLUDES = new Set(["omc-hud.mjs", "omc-hud.mjs.bak"]);
66
66
 
67
+ /**
68
+ * scripts/lib/*.mjs 자동 스캔.
69
+ * 수동 리스트 대신 glob으로 탐색하여 lib 파일 추가 시 sync 누락 방지.
70
+ */
71
+ function scanLibFiles(pluginRoot, claudeDir) {
72
+ const libDir = join(pluginRoot, "scripts", "lib");
73
+ if (!existsSync(libDir)) return [];
74
+ return readdirSync(libDir)
75
+ .sort()
76
+ .filter((f) => f.endsWith(".mjs"))
77
+ .map((f) => ({
78
+ src: join(libDir, f),
79
+ dst: join(claudeDir, "scripts", "lib", f),
80
+ label: `lib/${f}`,
81
+ }));
82
+ }
83
+
67
84
  /**
68
85
  * hub/workers/*.mjs + hub/ 루트의 worker 의존성 파일을 자동 스캔.
69
86
  * 수동 리스트 대신 glob으로 탐색하여 파일 추가 시 sync 누락 방지.
@@ -173,21 +190,7 @@ const SYNC_MAP = [
173
190
  dst: join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
174
191
  label: "tfx-batch-stats.mjs",
175
192
  },
176
- {
177
- src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-filter.mjs"),
178
- dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-filter.mjs"),
179
- label: "lib/mcp-filter.mjs",
180
- },
181
- {
182
- src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-server-catalog.mjs"),
183
- dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-server-catalog.mjs"),
184
- label: "lib/mcp-server-catalog.mjs",
185
- },
186
- {
187
- src: join(PLUGIN_ROOT, "scripts", "lib", "keyword-rules.mjs"),
188
- dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
189
- label: "lib/keyword-rules.mjs",
190
- },
193
+ ...scanLibFiles(PLUGIN_ROOT, CLAUDE_DIR),
191
194
  {
192
195
  src: join(PLUGIN_ROOT, "hub", "team", "agent-map.json"),
193
196
  dst: join(CLAUDE_DIR, "hub", "team", "agent-map.json"),
@@ -1193,12 +1193,14 @@ resolve_mcp_policy() {
1193
1193
  fi
1194
1194
 
1195
1195
  available_servers=$(get_cached_servers "$CLI_TYPE")
1196
- if [[ "$CLI_TYPE" == "codex" && "${TFX_CODEX_TRANSPORT:-auto}" != "mcp" ]]; then
1197
- available_servers=""
1196
+ # Codex exec 모드에서도 config.toml의 MCP 서버를 전부 시작하므로,
1197
+ # transport 모드와 관계없이 registered servers를 전달하여 불필요한 서버를
1198
+ # enabled=false로 비활성화해야 한다.
1199
+ # 캐시가 비어있으면 config.toml에서 직접 서버 목록을 추출한다.
1200
+ if [[ -z "$available_servers" && "$CLI_TYPE" == "codex" && -f "$_CODEX_CONFIG" ]]; then
1201
+ available_servers=$(sed -n 's/^\[mcp_servers\.\([^].]*\)\]$/\1/p' "$_CODEX_CONFIG" 2>/dev/null \
1202
+ | sort -u | tr '\n' ',' | sed 's/,$//')
1198
1203
  fi
1199
- # Codex 0.115+: 미등록 서버에 config override(enabled=true/false 모두)를 보내면
1200
- # "invalid transport" 에러 발생. 캐시 비어있으면 빈 문자열이 유지되어
1201
- # mcp-filter가 override를 생성하지 않는다.
1202
1204
 
1203
1205
  local -a cmd=(
1204
1206
  "$NODE_BIN" "$filter_script" shell
@@ -1398,6 +1400,53 @@ resolve_codex_mcp_script() {
1398
1400
  "$sd/hub/workers/codex-mcp.mjs" "$sd/../hub/workers/codex-mcp.mjs"
1399
1401
  }
1400
1402
 
1403
+ ## ── Config Swap: 프로필별 MCP 서버 필터링 ──
1404
+ # codex exec는 -c flag로 MCP enabled/disabled를 제어할 수 없다.
1405
+ # config.toml을 원자적으로 교체하여 불필요한 서버 시작을 방지한다.
1406
+ _codex_config_swap() {
1407
+ local action="$1" # "filter" or "restore"
1408
+ local config="$_CODEX_CONFIG"
1409
+ local backup="${config}.pre-exec"
1410
+
1411
+ if [[ "$action" == "filter" && -f "$config" ]]; then
1412
+ # MCP 프로필에서 허용된 서버 목록 추출
1413
+ local allowed_pat=""
1414
+ for flag in "${CODEX_CONFIG_FLAGS[@]}"; do
1415
+ if [[ "$flag" =~ mcp_servers\.([^.]+)\.enabled=true ]]; then
1416
+ [[ -n "$allowed_pat" ]] && allowed_pat="${allowed_pat}|"
1417
+ allowed_pat="${allowed_pat}${BASH_REMATCH[1]}"
1418
+ fi
1419
+ done
1420
+
1421
+ # 백업 생성 (이미 있으면 다른 워커가 swap 중 — 건드리지 않음)
1422
+ if [[ -f "$backup" ]]; then
1423
+ echo "[tfx-route] config.toml swap 스킵: 다른 워커가 사용 중" >&2
1424
+ return 0
1425
+ fi
1426
+ cp "$config" "$backup"
1427
+
1428
+ # awk로 필터링: 비허용 MCP 서버 섹션 제거, 나머지 그대로 유지
1429
+ awk -v keep="$allowed_pat" '
1430
+ BEGIN { skip=0 }
1431
+ /^\[mcp_servers\./ {
1432
+ name=$0; gsub(/^\[mcp_servers\./, "", name); gsub(/[\].].*/, "", name)
1433
+ if (keep == "" || name !~ "^(" keep ")$") { skip=1; next }
1434
+ else { skip=0 }
1435
+ }
1436
+ /^\[/ && !/^\[mcp_servers\./ { skip=0 }
1437
+ !skip { print }
1438
+ ' "$backup" > "$config"
1439
+
1440
+ local kept=0
1441
+ [[ -n "$allowed_pat" ]] && kept=$(echo "$allowed_pat" | tr '|' '\n' | wc -l | tr -d ' ')
1442
+ echo "[tfx-route] config.toml swap: ${kept}개 MCP 서버만 활성" >&2
1443
+
1444
+ elif [[ "$action" == "restore" && -f "$backup" ]]; then
1445
+ mv "$backup" "$config" 2>/dev/null
1446
+ echo "[tfx-route] config.toml 복원 완료" >&2
1447
+ fi
1448
+ }
1449
+
1401
1450
  run_codex_exec() {
1402
1451
  local prompt="$1"
1403
1452
  local use_tee_flag="$2"
@@ -1405,9 +1454,8 @@ run_codex_exec() {
1405
1454
  local worker_pid
1406
1455
  local -a codex_args=()
1407
1456
  read -r -a codex_args <<< "$CLI_ARGS"
1408
- if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
1409
- codex_args+=("${CODEX_CONFIG_FLAGS[@]}")
1410
- fi
1457
+ # -c flags는 codex exec에서 MCP enabled 제어 불가 — config swap으로 대체
1458
+ # config swap은 codex 블록 최상단(_codex_config_swap "filter")에서 실행됨
1411
1459
 
1412
1460
  if [[ "$use_tee_flag" == "true" ]]; then
1413
1461
  "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" < /dev/null 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
@@ -1656,6 +1704,12 @@ FALLBACK_EOF
1656
1704
  fi
1657
1705
 
1658
1706
  if [[ "$CLI_TYPE" == "codex" ]]; then
1707
+ # Config swap: 프로필에 맞는 MCP 서버만 남긴 임시 config 적용
1708
+ # run_codex_mcp / run_codex_exec 어느 경로든 적용되도록 최상단에서 실행
1709
+ _codex_config_swap "filter"
1710
+ # swap 후 config override 플래그 클리어 — 제거된 서버에 override 보내면 "invalid transport" 에러
1711
+ CODEX_CONFIG_FLAGS=()
1712
+ CODEX_CONFIG_JSON="{}"
1659
1713
  codex_transport_effective="exec"
1660
1714
  if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
1661
1715
  run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
@@ -1676,6 +1730,8 @@ FALLBACK_EOF
1676
1730
  codex_transport_effective="exec"
1677
1731
  fi
1678
1732
  echo "[tfx-route] codex_transport_effective=$codex_transport_effective" >&2
1733
+ # Config swap 복원 (성공/실패 관계없이)
1734
+ _codex_config_swap "restore"
1679
1735
 
1680
1736
  elif [[ "$CLI_TYPE" == "gemini" ]]; then
1681
1737
  local gemini_model