triflux 4.2.3 → 4.2.5

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.
package/bin/triflux.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  // triflux CLI — setup, doctor, version
3
3
  import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync, openSync, closeSync } from "fs";
4
4
  import { join, dirname } from "path";
@@ -1045,13 +1045,7 @@ async function cmdDoctor(options = {}) {
1045
1045
  shells: codexCli.shells,
1046
1046
  ...(codexCli.fix ? { fix: codexCli.fix } : {}),
1047
1047
  });
1048
- if (which("codex")) {
1049
- if (process.env.OPENAI_API_KEY) {
1050
- ok("OPENAI_API_KEY 설정됨");
1051
- } else {
1052
- warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
1053
- }
1054
- }
1048
+ // API 키 검사 제거 — bash exec 기반이므로 API 키 불필요
1055
1049
 
1056
1050
  // 4. Codex Profiles
1057
1051
  section("Codex Profiles");
@@ -1090,13 +1084,7 @@ async function cmdDoctor(options = {}) {
1090
1084
  shells: geminiCli.shells,
1091
1085
  ...(geminiCli.fix ? { fix: geminiCli.fix } : {}),
1092
1086
  });
1093
- if (which("gemini")) {
1094
- if (process.env.GEMINI_API_KEY) {
1095
- ok("GEMINI_API_KEY 설정됨");
1096
- } else {
1097
- warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
1098
- }
1099
- }
1087
+ // API 키 검사 제거 — bash exec 기반이므로 API 키 불필요
1100
1088
 
1101
1089
  // 6. Claude Code
1102
1090
  section(`Claude Code ${AMBER}●${RESET}`);
package/hub/server.mjs CHANGED
@@ -718,7 +718,7 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
718
718
  await pipe.start();
719
719
  await assignCallbacks.start();
720
720
 
721
- return new Promise((resolve, reject) => {
721
+ return new Promise((resolveHub, reject) => {
722
722
  httpServer.listen(port, host, () => {
723
723
  const info = {
724
724
  port,
@@ -763,10 +763,11 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessi
763
763
  store.close();
764
764
  try { unlinkSync(PID_FILE); } catch {}
765
765
  try { unlinkSync(TOKEN_FILE); } catch {}
766
+ httpServer.closeAllConnections();
766
767
  await new Promise((resolveClose) => httpServer.close(resolveClose));
767
768
  };
768
769
 
769
- resolve({
770
+ resolveHub({
770
771
  ...info,
771
772
  httpServer,
772
773
  store,
@@ -10,12 +10,12 @@ import * as fs from "node:fs/promises";
10
10
  import os from "node:os";
11
11
  import path from "node:path";
12
12
 
13
- const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
14
- export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
15
- const ROUTE_LOG_RE = /\[tfx-route\]/i;
16
- const ROUTE_COMMAND_RE = /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
17
- const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
18
- const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
13
+ const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
14
+ export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
15
+ const ROUTE_LOG_RE = /\[tfx-route\]/i;
16
+ const ROUTE_COMMAND_RE = /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
17
+ const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
18
+ const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
19
19
 
20
20
  function inferWorkerIndex(agentName = "") {
21
21
  const match = /(\d+)(?!.*\d)/.exec(agentName);
@@ -60,46 +60,46 @@ export function buildSlimWrapperAgent(cli, opts = {}) {
60
60
  * @param {string} [input.promptText]
61
61
  * @param {string} [input.stdoutText]
62
62
  * @param {string} [input.stderrText]
63
- * @returns {{
64
- * expectedRouteInvocation: boolean,
65
- * promptMentionsRoute: boolean,
66
- * sawRouteCommand: boolean,
67
- * sawRouteLog: boolean,
68
- * sawDirectToolBypass: boolean,
69
- * usedRoute: boolean,
70
- * abnormal: boolean,
71
- * reason: string|null,
72
- * }}
73
- */
74
- export function verifySlimWrapperRouteExecution(input = {}) {
63
+ * @returns {{
64
+ * expectedRouteInvocation: boolean,
65
+ * promptMentionsRoute: boolean,
66
+ * sawRouteCommand: boolean,
67
+ * sawRouteLog: boolean,
68
+ * sawDirectToolBypass: boolean,
69
+ * usedRoute: boolean,
70
+ * abnormal: boolean,
71
+ * reason: string|null,
72
+ * }}
73
+ */
74
+ export function verifySlimWrapperRouteExecution(input = {}) {
75
75
  const promptText = String(input.promptText || "");
76
76
  const stdoutText = String(input.stdoutText || "");
77
77
  const stderrText = String(input.stderrText || "");
78
- const combinedLogs = `${stdoutText}\n${stderrText}`;
79
- const promptMentionsRoute = ROUTE_PROMPT_RE.test(promptText);
80
- const sawRouteCommand = ROUTE_COMMAND_RE.test(combinedLogs);
81
- const sawRouteLog = ROUTE_LOG_RE.test(combinedLogs);
82
- const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
83
- const usedRoute = sawRouteCommand || sawRouteLog;
84
- const expectedRouteInvocation = promptMentionsRoute;
85
- const abnormal = expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
86
- const reason = !abnormal
87
- ? null
88
- : sawDirectToolBypass
89
- ? "direct_tool_bypass_detected"
90
- : "missing_tfx_route_evidence";
91
-
92
- return {
93
- expectedRouteInvocation,
94
- promptMentionsRoute,
95
- sawRouteCommand,
96
- sawRouteLog,
97
- sawDirectToolBypass,
98
- usedRoute,
99
- abnormal,
100
- reason,
101
- };
102
- }
78
+ const combinedLogs = `${stdoutText}\n${stderrText}`;
79
+ const promptMentionsRoute = ROUTE_PROMPT_RE.test(promptText);
80
+ const sawRouteCommand = ROUTE_COMMAND_RE.test(combinedLogs);
81
+ const sawRouteLog = ROUTE_LOG_RE.test(combinedLogs);
82
+ const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
83
+ const usedRoute = sawRouteCommand || sawRouteLog;
84
+ const expectedRouteInvocation = promptMentionsRoute;
85
+ const abnormal = expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
86
+ const reason = !abnormal
87
+ ? null
88
+ : sawDirectToolBypass
89
+ ? "direct_tool_bypass_detected"
90
+ : "missing_tfx_route_evidence";
91
+
92
+ return {
93
+ expectedRouteInvocation,
94
+ promptMentionsRoute,
95
+ sawRouteCommand,
96
+ sawRouteLog,
97
+ sawDirectToolBypass,
98
+ usedRoute,
99
+ abnormal,
100
+ reason,
101
+ };
102
+ }
103
103
 
104
104
  /**
105
105
  * role/mcp_profile별 tfx-route.sh 기본 timeout (초)
@@ -108,10 +108,21 @@ export function verifySlimWrapperRouteExecution(input = {}) {
108
108
  * @param {string} mcpProfile — MCP 프로필
109
109
  * @returns {number} timeout(초)
110
110
  */
111
- function getRouteTimeout(role, mcpProfile) {
112
- if (mcpProfile === "analyze" || mcpProfile === "review") return 3600;
113
- if (role === "architect" || role === "analyst") return 3600;
114
- return 1080; // 기본 18분
111
+ function getRouteTimeout(role, _mcpProfile) {
112
+ // tfx-route.sh route_agent()의 DEFAULT_TIMEOUT 기반, 최소 1080초(18분) 보장.
113
+ // Bash timeout = + 60초 여유. 짧은 역할도 네트워크/스케줄 지연 대비.
114
+ const TIMEOUTS = {
115
+ 'build-fixer': 1080, debugger: 1080, executor: 1080,
116
+ 'deep-executor': 3600, architect: 3600, planner: 3600,
117
+ critic: 3600, analyst: 3600, scientist: 1800,
118
+ 'scientist-deep': 3600, 'document-specialist': 1800,
119
+ 'code-reviewer': 1800, 'security-reviewer': 1800,
120
+ 'quality-reviewer': 1800, verifier: 1800,
121
+ designer: 1080, writer: 1080,
122
+ explore: 1080, 'test-engineer': 1080, 'qa-tester': 1080,
123
+ spark: 600,
124
+ };
125
+ return TIMEOUTS[role] || 1080;
115
126
  }
116
127
 
117
128
  /**
@@ -6,7 +6,7 @@ import { randomUUID } from 'node:crypto';
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
7
  import { dirname, isAbsolute, resolve } from 'node:path';
8
8
  import process from 'node:process';
9
- import { fileURLToPath } from 'node:url';
9
+ import { fileURLToPath, pathToFileURL } from 'node:url';
10
10
 
11
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -14,15 +14,25 @@ import * as z from 'zod';
14
14
 
15
15
  import { CodexMcpWorker } from './codex-mcp.mjs';
16
16
  import { GeminiWorker } from './gemini-worker.mjs';
17
- import {
17
+
18
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
19
+
20
+ // mcp-filter.mjs 동적 해석 — 프로젝트(hub/workers/)와 배포(scripts/hub/workers/) 양쪽 대응
21
+ const MCP_FILTER_CANDIDATES = [
22
+ resolve(SCRIPT_DIR, '../../scripts/lib/mcp-filter.mjs'), // 프로젝트 원본
23
+ resolve(SCRIPT_DIR, '../../lib/mcp-filter.mjs'), // 배포 (~/.claude/scripts/)
24
+ ];
25
+ const mcpFilterPath = MCP_FILTER_CANDIDATES.find((p) => existsSync(p));
26
+ if (!mcpFilterPath) {
27
+ throw new Error(`mcp-filter.mjs not found. candidates: ${MCP_FILTER_CANDIDATES.join(', ')}`);
28
+ }
29
+ const {
18
30
  buildPromptHint,
19
31
  getCodexMcpConfig,
20
32
  getGeminiAllowedServers,
21
33
  resolveMcpProfile,
22
34
  SUPPORTED_MCP_PROFILES,
23
- } from '../../scripts/lib/mcp-filter.mjs';
24
-
25
- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
35
+ } = await import(pathToFileURL(mcpFilterPath).href);
26
36
  const SERVER_INFO = { name: 'triflux-delegator', version: '1.0.0' };
27
37
  const DEFAULT_CONTEXT_BYTES = 32 * 1024;
28
38
  const DEFAULT_ROUTE_TIMEOUT_SEC = 120;
@@ -70,6 +80,8 @@ const CODEX_PROFILE_BY_AGENT = Object.freeze({
70
80
  'scientist-deep': 'thorough',
71
81
  'document-specialist': 'high',
72
82
  verifier: 'thorough',
83
+ designer: 'high', // Gemini primary, codex fallback용
84
+ writer: 'high', // Gemini primary, codex fallback용
73
85
  spark: 'spark_fast',
74
86
  });
75
87
 
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "triflux",
3
- "version": "4.2.3",
4
- "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
- "type": "module",
6
- "bin": {
7
- "triflux": "bin/triflux.mjs",
8
- "tfx": "bin/triflux.mjs",
9
- "tfl": "bin/triflux.mjs",
10
- "tfx-setup": "bin/tfx-setup.mjs",
11
- "tfx-doctor": "bin/tfx-doctor.mjs"
12
- },
13
- "files": [
14
- "bin",
15
- "hub",
16
- "skills",
17
- "!**/failure-reports",
18
- "scripts",
19
- "hooks",
20
- "hud",
21
- ".claude-plugin",
22
- ".mcp.json",
23
- "README.md",
24
- "README.ko.md",
25
- "LICENSE"
26
- ],
27
- "scripts": {
28
- "setup": "node scripts/setup.mjs",
29
- "preinstall": "node scripts/preinstall.mjs",
30
- "postinstall": "node scripts/setup.mjs",
31
- "test": "node --test --test-force-exit --test-concurrency=1 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
32
- "test:unit": "node --test --test-force-exit --test-concurrency=1 tests/unit/**/*.test.mjs",
33
- "test:integration": "node --test --test-force-exit --test-concurrency=1 tests/integration/**/*.test.mjs",
34
- "test:route-smoke": "node --test scripts/test-tfx-route-no-claude-native.mjs"
35
- },
36
- "engines": {
37
- "node": ">=18.0.0"
38
- },
39
- "repository": {
40
- "type": "git",
41
- "url": "git+https://github.com/tellang/triflux.git"
42
- },
43
- "homepage": "https://github.com/tellang/triflux#readme",
44
- "author": "tellang",
45
- "license": "MIT",
46
- "dependencies": {
47
- "@modelcontextprotocol/sdk": "^1.27.1",
48
- "better-sqlite3": "^12.6.2",
49
- "systray2": "^2.1.4"
50
- },
51
- "keywords": [
52
- "claude-code",
53
- "plugin",
54
- "codex",
55
- "gemini",
56
- "cli-routing",
57
- "orchestration",
58
- "multi-model",
59
- "triflux",
60
- "tfx"
61
- ]
62
- }
1
+ {
2
+ "name": "triflux",
3
+ "version": "4.2.5",
4
+ "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
+ "type": "module",
6
+ "bin": {
7
+ "triflux": "bin/triflux.mjs",
8
+ "tfx": "bin/triflux.mjs",
9
+ "tfl": "bin/triflux.mjs",
10
+ "tfx-setup": "bin/tfx-setup.mjs",
11
+ "tfx-doctor": "bin/tfx-doctor.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "hub",
16
+ "skills",
17
+ "!**/failure-reports",
18
+ "scripts",
19
+ "hooks",
20
+ "hud",
21
+ ".claude-plugin",
22
+ ".mcp.json",
23
+ "README.md",
24
+ "README.ko.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "setup": "node scripts/setup.mjs",
29
+ "preinstall": "node scripts/preinstall.mjs",
30
+ "postinstall": "node scripts/setup.mjs",
31
+ "test": "node --test --test-force-exit --test-concurrency=1 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
32
+ "test:unit": "node --test --test-force-exit --test-concurrency=1 tests/unit/**/*.test.mjs",
33
+ "test:integration": "node --test --test-force-exit --test-concurrency=1 tests/integration/**/*.test.mjs",
34
+ "test:route-smoke": "node --test scripts/test-tfx-route-no-claude-native.mjs"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/tellang/triflux.git"
42
+ },
43
+ "homepage": "https://github.com/tellang/triflux#readme",
44
+ "author": "tellang",
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.27.1",
48
+ "better-sqlite3": "^12.6.2",
49
+ "systray2": "^2.1.4"
50
+ },
51
+ "keywords": [
52
+ "claude-code",
53
+ "plugin",
54
+ "codex",
55
+ "gemini",
56
+ "cli-routing",
57
+ "orchestration",
58
+ "multi-model",
59
+ "triflux",
60
+ "tfx"
61
+ ]
62
+ }
@@ -71,8 +71,21 @@ const PROFILE_DEFINITIONS = Object.freeze({
71
71
  ]),
72
72
  }),
73
73
  }),
74
+ analyze: Object.freeze({
75
+ description: '분석/설계 워커용. 추론 + 검색 MCP 허용',
76
+ allowedServers: Object.freeze(['context7', 'brave-search', 'tavily', 'exa', 'sequential-thinking']),
77
+ alwaysOnServers: Object.freeze(['context7', 'sequential-thinking']),
78
+ maxSearchServers: 2,
79
+ allowedToolsByServer: Object.freeze({
80
+ context7: Object.freeze(['resolve-library-id', 'query-docs']),
81
+ 'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
82
+ exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
83
+ tavily: Object.freeze(['tavily_search', 'tavily_extract']),
84
+ 'sequential-thinking': Object.freeze(['sequentialthinking']),
85
+ }),
86
+ }),
74
87
  explore: Object.freeze({
75
- description: '탐색 워커용. 읽기/검색 중심 MCP만 허용',
88
+ description: '탐색/리서치 워커용. 읽기/검색 중심 MCP만 허용',
76
89
  allowedServers: Object.freeze(['context7', 'brave-search', 'tavily', 'exa']),
77
90
  alwaysOnServers: Object.freeze(['context7']),
78
91
  maxSearchServers: 2,
@@ -116,7 +129,7 @@ const PROFILE_DEFINITIONS = Object.freeze({
116
129
 
117
130
  export const LEGACY_PROFILE_ALIASES = Object.freeze({
118
131
  implement: 'executor',
119
- analyze: 'explore',
132
+ analyze: 'analyze',
120
133
  review: 'reviewer',
121
134
  docs: 'writer',
122
135
  minimal: 'default',
@@ -147,15 +160,17 @@ function resolveAutoProfile(agentType = '') {
147
160
  case 'build-fixer':
148
161
  case 'debugger':
149
162
  case 'deep-executor':
163
+ return 'executor';
150
164
  case 'test-engineer':
151
165
  case 'qa-tester':
152
- return 'executor';
166
+ return 'none';
153
167
  case 'designer':
154
168
  return 'designer';
155
169
  case 'architect':
156
170
  case 'planner':
157
171
  case 'critic':
158
172
  case 'analyst':
173
+ return 'analyze';
159
174
  case 'scientist':
160
175
  case 'scientist-deep':
161
176
  case 'document-specialist':
@@ -474,17 +489,28 @@ export function getGeminiAllowedServers(options = {}) {
474
489
  export function getCodexMcpConfig(options = {}) {
475
490
  const allowedServers = new Set(resolveAllowedServers(options));
476
491
  const resolvedProfile = resolveMcpProfile(options.agentType, options.requestedProfile);
492
+ // Codex에 실제 등록된 서버만 대상으로 config override 생성.
493
+ // 미등록 서버에 enabled=false를 보내면 "invalid transport" 에러 발생.
494
+ const registeredServers = parseAvailableServers(options.availableServers);
495
+ // Codex 0.115+: 미등록 서버에 config override를 보내면 "invalid transport" 에러.
496
+ // 등록 서버 정보가 없으면 override를 생성하지 않는다 (안전 기본값).
497
+ if (registeredServers.length === 0) {
498
+ return { mcp_servers: {} };
499
+ }
500
+ const targetServers = registeredServers;
501
+
477
502
  if (resolvedProfile === 'none') {
478
- return {
479
- mcp_servers: Object.fromEntries(KNOWN_MCP_SERVERS.map((server) => [server, { enabled: false }])),
480
- };
503
+ // Codex 0.115+: transport 없는 서버에 enabled=false를 보내면 "invalid transport" 에러.
504
+ // 비허용 서버는 override에서 제외하고, 허용 서버만 명시적으로 설정한다.
505
+ return { mcp_servers: {} };
481
506
  }
482
507
 
483
508
  const config = { mcp_servers: {} };
484
509
  const allowedToolsByServer = getProfileDefinition(resolvedProfile).allowedToolsByServer;
485
- for (const server of KNOWN_MCP_SERVERS) {
510
+ for (const server of targetServers) {
511
+ // Codex 0.115+: transport 없는 서버에 enabled=false를 보내면 "invalid transport" 에러.
512
+ // 비허용 서버는 override에서 제외한다 (Codex 기본 설정이 유지됨).
486
513
  if (!allowedServers.has(server)) {
487
- config.mcp_servers[server] = { enabled: false };
488
514
  continue;
489
515
  }
490
516
 
@@ -558,20 +558,23 @@ route_agent() {
558
558
  CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
559
559
  CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
560
560
 
561
- # ─── Claude 네이티브 ───
561
+ # ─── 탐색/검증/테스트 (Codex 우선, claude-native fallback은 apply_cli_mode auto에서) ───
562
562
  explore)
563
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
564
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
563
+ CLI_TYPE="codex"; CLI_CMD="codex"
564
+ CLI_ARGS="exec --profile fast ${codex_base}"
565
+ CLI_EFFORT="fast"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
565
566
  verifier)
566
567
  CLI_TYPE="codex"; CLI_CMD="codex"
567
568
  CLI_ARGS="exec --profile thorough ${codex_base} review"
568
569
  CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
569
570
  test-engineer)
570
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
571
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
571
+ CLI_TYPE="codex"; CLI_CMD="codex"
572
+ CLI_ARGS="exec ${codex_base}"
573
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1200; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
572
574
  qa-tester)
573
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
574
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
575
+ CLI_TYPE="codex"; CLI_CMD="codex"
576
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
577
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1200; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
575
578
 
576
579
  # ─── 경량 ───
577
580
  spark)
@@ -849,7 +852,10 @@ resolve_mcp_policy() {
849
852
  fi
850
853
 
851
854
  available_servers=$(get_cached_servers "$CLI_TYPE")
852
- [[ -z "$available_servers" ]] && available_servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
855
+ # Codex 0.115+: 미등록 서버에 config override(enabled=true/false 모두)를 보내면
856
+ # "invalid transport" 에러 발생. 캐시 비어있으면 빈 문자열로 유지하여
857
+ # mcp-filter가 override를 생성하지 않도록 한다.
858
+ [[ -z "$available_servers" ]] && available_servers=""
853
859
 
854
860
  local -a cmd=(
855
861
  "$NODE_BIN" "$filter_script" shell