@dmsdc-ai/aigentry-deliberation 0.0.27 → 0.0.29

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/index.js CHANGED
@@ -22,11 +22,11 @@ if (_cliArg === "--help" || _cliArg === "-h") {
22
22
  MCP Deliberation Server
23
23
 
24
24
  Usage:
25
- npx @dmsdc-ai/aigentry-deliberation install 설치 (MCP 서버 등록)
26
- npx @dmsdc-ai/aigentry-deliberation uninstall 제거
27
- npx @dmsdc-ai/aigentry-deliberation MCP 서버 실행 (stdio)
25
+ npx @dmsdc-ai/aigentry-deliberation install Install (register MCP server)
26
+ npx @dmsdc-ai/aigentry-deliberation uninstall Uninstall
27
+ npx @dmsdc-ai/aigentry-deliberation Run MCP server (stdio)
28
28
 
29
- 설치 Claude Code 재시작하면 자동으로 사용 가능합니다.
29
+ After installation, restart Claude Code to start using it.
30
30
  `);
31
31
  process.exit(0);
32
32
  }
@@ -34,34 +34,34 @@ Usage:
34
34
  /**
35
35
  * MCP Deliberation Server (Global) — Multi-Session + Transport Routing + Cross-Platform + BrowserControlPort
36
36
  *
37
- * 모든 프로젝트에서 사용 가능한 AI deliberation 서버.
38
- * 동시에 여러 deliberation을 병렬 진행할 있다.
37
+ * A global AI deliberation server usable across all projects.
38
+ * Multiple deliberations can run in parallel simultaneously.
39
39
  *
40
- * 상태 저장: $INSTALL_DIR/state/{project-slug}/sessions/{id}.json
40
+ * State storage: $INSTALL_DIR/state/{project-slug}/sessions/{id}.json
41
41
  * macOS/Linux: ~/.local/lib/mcp-deliberation/
42
42
  * Windows: %LOCALAPPDATA%/mcp-deliberation/
43
43
  *
44
44
  * Tools:
45
- * deliberation_start 토론 시작 → session_id 반환
46
- * deliberation_status 세션 상태 조회 (session_id 선택적)
47
- * deliberation_list_active 진행 중인 모든 세션 목록
48
- * deliberation_context 프로젝트 컨텍스트 로드
49
- * deliberation_respond 응답 제출 (session_id 필수)
50
- * deliberation_history 토론 기록 조회 (session_id 선택적)
51
- * deliberation_synthesize 합성 보고서 생성 (session_id 선택적)
52
- * deliberation_list 과거 아카이브 목록
53
- * deliberation_reset 세션 초기화 (session_id 선택적, 없으면 전체)
54
- * deliberation_speaker_candidates 선택 가능한 스피커 후보(로컬 CLI + 브라우저 LLM ) 조회
55
- * deliberation_browser_llm_tabs 브라우저 LLM 목록 조회
56
- * deliberation_browser_auto_turn 브라우저 LLM에 자동으로 턴을 전송하고 응답을 수집 (CDP 기반)
57
- * deliberation_cli_auto_turn CLI speaker 자동으로 턴을 전송하고 응답을 수집
58
- * deliberation_request_review 코드 리뷰 요청 (CLI 리뷰어 자동 호출, sync/async 모드)
59
- * decision_start 의사결정 세션 시작 (템플릿 지원)
60
- * decision_status 의사결정 세션 상태 조회
61
- * decision_respond user_probe 갈등 질문에 대한 사용자 응답 제출
62
- * decision_resume 일시 중지된 세션 재개
63
- * decision_history 과거 의사결정 기록 조회
64
- * decision_templates Micro-Decision 템플릿 목록
45
+ * deliberation_start Start a new deliberation returns session_id
46
+ * deliberation_status Query session status (session_id optional)
47
+ * deliberation_list_active List all active sessions
48
+ * deliberation_context Load project context
49
+ * deliberation_respond Submit a response (session_id required)
50
+ * deliberation_history Query deliberation history (session_id optional)
51
+ * deliberation_synthesize Generate synthesis report (session_id optional)
52
+ * deliberation_list List past archives
53
+ * deliberation_reset Reset session (session_id optional, resets all if omitted)
54
+ * deliberation_speaker_candidates Query available speaker candidates (local CLI + browser LLM tabs)
55
+ * deliberation_browser_llm_tabs Query browser LLM tab list
56
+ * deliberation_browser_auto_turn Auto-send turn to browser LLM and collect response (CDP-based)
57
+ * deliberation_cli_auto_turn Auto-send turn to CLI speaker and collect response
58
+ * deliberation_request_review Request code review (auto-invoke CLI reviewers, sync/async mode)
59
+ * decision_start Start a new decision session (template support)
60
+ * decision_status Query decision session status
61
+ * decision_respond Submit user responses to user_probe conflict questions
62
+ * decision_resume Resume a paused session
63
+ * decision_history Query past decision history
64
+ * decision_templates Micro-Decision template list
65
65
  */
66
66
 
67
67
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -81,6 +81,7 @@ import {
81
81
  generateConflictQuestions, buildSynthesis, buildActionPlan,
82
82
  loadTemplates, matchTemplate,
83
83
  } from "./decision-engine.js";
84
+ import { detectLang, t } from "./i18n.js";
84
85
 
85
86
  // ── Paths ──────────────────────────────────────────────────────
86
87
 
@@ -297,19 +298,19 @@ function applyRolePreset(preset, speakers) {
297
298
 
298
299
  const DEGRADATION_TIERS = {
299
300
  monitoring: {
300
- tier1: { name: "tmux", description: "tmux 실시간 모니터링 윈도우", check: () => commandExistsInPath("tmux") },
301
- tier2: { name: "logfile", description: "로그 파일 tail 모니터링", check: () => true },
302
- tier3: { name: "silent", description: "모니터링 없음 (로그만 기록)", check: () => true },
301
+ tier1: { name: "tmux", description: "tmux real-time monitoring window", check: () => commandExistsInPath("tmux") },
302
+ tier2: { name: "logfile", description: "Log file tail monitoring", check: () => true },
303
+ tier3: { name: "silent", description: "No monitoring (log only)", check: () => true },
303
304
  },
304
305
  browser: {
305
- tier1: { name: "cdp_auto", description: "CDP 자동 전송/수집", check: async () => { try { const res = await fetch("http://127.0.0.1:9222/json/version", { signal: AbortSignal.timeout(2000) }); return res.ok; } catch { return false; } } },
306
- tier2: { name: "clipboard", description: "클립보드 기반 수동 전달", check: () => true },
307
- tier3: { name: "manual", description: "완전 수동 복사/붙여넣기", check: () => true },
306
+ tier1: { name: "cdp_auto", description: "CDP auto send/collect", check: async () => { try { const res = await fetch("http://127.0.0.1:9222/json/version", { signal: AbortSignal.timeout(2000) }); return res.ok; } catch { return false; } } },
307
+ tier2: { name: "clipboard", description: "Clipboard-based manual transfer", check: () => true },
308
+ tier3: { name: "manual", description: "Fully manual copy/paste", check: () => true },
308
309
  },
309
310
  terminal: {
310
- tier1: { name: "auto_open", description: "터미널 자동 오픈", check: () => process.platform === "darwin" || process.platform === "linux" || process.platform === "win32" },
311
- tier2: { name: "none", description: "터미널 자동 오픈 불가", check: () => true },
312
- tier3: { name: "none", description: "터미널 자동 오픈 불가", check: () => true },
311
+ tier1: { name: "auto_open", description: "Auto-open terminal app", check: () => process.platform === "darwin" || process.platform === "linux" || process.platform === "win32" },
312
+ tier2: { name: "none", description: "Cannot auto-open terminal", check: () => true },
313
+ tier3: { name: "none", description: "Cannot auto-open terminal", check: () => true },
313
314
  },
314
315
  };
315
316
 
@@ -338,7 +339,7 @@ function formatDegradationReport(levels) {
338
339
  return lines.join("\n");
339
340
  }
340
341
 
341
- const PRODUCT_DISCLAIMER = "ℹ️ 도구는 외부 웹사이트를 영구 수정하지 않습니다. 브라우저 문맥을 읽기 전용으로 참조하여 발화자를 라우팅합니다.";
342
+ const PRODUCT_DISCLAIMER = "ℹ️ This tool does not permanently modify external websites. It reads browser context in read-only mode to route speakers.";
342
343
  const LOCKS_SUBDIR = ".locks";
343
344
  const LOCK_RETRY_MS = 25;
344
345
  const LOCK_TIMEOUT_MS = 8000;
@@ -396,7 +397,7 @@ function safeToolHandler(toolName, handler) {
396
397
  } catch (error) {
397
398
  const message = formatRuntimeError(error);
398
399
  appendRuntimeLog("ERROR", `${toolName}: ${message}`);
399
- return { content: [{ type: "text", text: `❌ ${toolName} 실패: ${message}` }] };
400
+ return { content: [{ type: "text", text: t(`❌ ${toolName} failed: ${message}`, `❌ ${toolName} 실패: ${message}`, "en") }] };
400
401
  }
401
402
  };
402
403
  }
@@ -675,7 +676,7 @@ function resolveClipboardWriter() {
675
676
  function readClipboardText() {
676
677
  const tool = resolveClipboardReader();
677
678
  if (!tool) {
678
- throw new Error("지원되는 클립보드 읽기 명령이 없습니다 (pbpaste/wl-paste/xclip/xsel ).");
679
+ throw new Error("No supported clipboard read command found (pbpaste/wl-paste/xclip/xsel etc).");
679
680
  }
680
681
  return execFileSync(tool.cmd, tool.args, {
681
682
  encoding: "utf-8",
@@ -687,7 +688,7 @@ function readClipboardText() {
687
688
  function writeClipboardText(text) {
688
689
  const tool = resolveClipboardWriter();
689
690
  if (!tool) {
690
- throw new Error("지원되는 클립보드 쓰기 명령이 없습니다 (pbcopy/wl-copy/xclip/xsel ).");
691
+ throw new Error("No supported clipboard write command found (pbcopy/wl-copy/xclip/xsel etc).");
691
692
  }
692
693
  execFileSync(tool.cmd, tool.args, {
693
694
  input: text,
@@ -739,7 +740,7 @@ function parseInjectedBrowserTabsFromEnv() {
739
740
  try {
740
741
  const parsed = JSON.parse(raw);
741
742
  if (!Array.isArray(parsed)) {
742
- return { tabs: [], note: "DELIBERATION_BROWSER_TABS_JSON 형식 오류: JSON 배열이어야 합니다." };
743
+ return { tabs: [], note: "DELIBERATION_BROWSER_TABS_JSON format error: must be a JSON array." };
743
744
  }
744
745
 
745
746
  const tabs = dedupeBrowserTabs(parsed.map(item => ({
@@ -749,11 +750,11 @@ function parseInjectedBrowserTabsFromEnv() {
749
750
  })));
750
751
  return {
751
752
  tabs,
752
- note: tabs.length > 0 ? `환경변수 주입 사용: ${tabs.length}개` : "DELIBERATION_BROWSER_TABS_JSON에 유효한 LLM URL이 없습니다.",
753
+ note: tabs.length > 0 ? `Environment variable tab injection: ${tabs.length} tabs` : "No valid LLM URLs found in DELIBERATION_BROWSER_TABS_JSON.",
753
754
  };
754
755
  } catch (error) {
755
756
  const reason = error instanceof Error ? error.message : "unknown error";
756
- return { tabs: [], note: `DELIBERATION_BROWSER_TABS_JSON 파싱 실패: ${reason}` };
757
+ return { tabs: [], note: `Failed to parse DELIBERATION_BROWSER_TABS_JSON: ${reason}` };
757
758
  }
758
759
  }
759
760
 
@@ -833,7 +834,7 @@ function inferBrowserFromCdpEndpoint(endpoint) {
833
834
  function summarizeFailures(items = [], max = 3) {
834
835
  if (!Array.isArray(items) || items.length === 0) return null;
835
836
  const shown = items.slice(0, max);
836
- const suffix = items.length > max ? ` ${items.length - max}개` : "";
837
+ const suffix = items.length > max ? ` and ${items.length - max} more` : "";
837
838
  return `${shown.join(", ")}${suffix}`;
838
839
  }
839
840
 
@@ -872,14 +873,14 @@ async function collectBrowserLlmTabsViaCdp() {
872
873
  const failSummary = summarizeFailures(failures);
873
874
  return {
874
875
  tabs: uniqTabs,
875
- note: failSummary ? `일부 CDP 엔드포인트 접근 실패: ${failSummary}` : null,
876
+ note: failSummary ? `Some CDP endpoint access failed: ${failSummary}` : null,
876
877
  };
877
878
  }
878
879
 
879
880
  const failSummary = summarizeFailures(failures);
880
881
  return {
881
882
  tabs: [],
882
- note: `CDP에서 LLM 탭을 찾지 못했습니다. 브라우저를 --remote-debugging-port=9222 실행하거나 DELIBERATION_BROWSER_TABS_JSON으로 목록을 주입하세요.${failSummary ? ` (실패: ${failSummary})` : ""}`,
883
+ note: `No LLM tabs found via CDP. Run browser with --remote-debugging-port=9222 or inject tab list via DELIBERATION_BROWSER_TABS_JSON.${failSummary ? ` (failed: ${failSummary})` : ""}`,
883
884
  };
884
885
  }
885
886
 
@@ -909,7 +910,7 @@ async function ensureCdpAvailable() {
909
910
  if (!chromeBin) {
910
911
  return {
911
912
  available: false,
912
- reason: "Chrome/Chromium 찾을 없습니다. google-chrome 또는 chromium 설치하고 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
913
+ reason: "Chrome/Chromium not found. Install google-chrome or chromium and run with --remote-debugging-port=9222.",
913
914
  };
914
915
  }
915
916
  const googleDir = path.join(os.homedir(), ".config", "google-chrome");
@@ -930,7 +931,7 @@ async function ensureCdpAvailable() {
930
931
  if (!chromeBin) {
931
932
  return {
932
933
  available: false,
933
- reason: "Chrome/Edge 찾을 없습니다. Chrome 설치하거나 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
934
+ reason: "Chrome/Edge not found. Install Chrome or run with --remote-debugging-port=9222.",
934
935
  };
935
936
  }
936
937
  const chromeDir = path.join(localAppData, "Google", "Chrome", "User Data");
@@ -939,22 +940,34 @@ async function ensureCdpAvailable() {
939
940
  } else {
940
941
  return {
941
942
  available: false,
942
- reason: "Chrome CDP 활성화할 없습니다. Chrome을 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
943
+ reason: "Cannot activate Chrome CDP. Run Chrome with --remote-debugging-port=9222.",
943
944
  };
944
945
  }
945
946
 
946
947
  // Chrome 145+ requires --user-data-dir for CDP to work.
947
948
  // The default data dir is rejected, so we copy the profile to ~/.chrome-cdp.
949
+ // Profile can be set via env DELIBERATION_CHROME_PROFILE or config.chrome_profile (e.g., "Profile 1").
948
950
  const cdpDataDir = path.join(os.homedir(), ".chrome-cdp");
949
- const profileDir = "Default";
951
+ const cdpConfig = loadDeliberationConfig();
952
+ const profileDir = process.env.DELIBERATION_CHROME_PROFILE || cdpConfig.chrome_profile || "Default";
950
953
 
951
954
  try {
952
955
  if (chromeUserDataDir) {
953
956
  const srcProfile = path.join(chromeUserDataDir, profileDir);
954
957
  const dstProfile = path.join(cdpDataDir, profileDir);
955
- if (!fs.existsSync(dstProfile) && fs.existsSync(srcProfile)) {
958
+ // Track which profile was copied; re-copy if profile changed
959
+ const profileMarker = path.join(cdpDataDir, ".cdp-profile");
960
+ const lastProfile = fs.existsSync(profileMarker) ? fs.readFileSync(profileMarker, "utf8").trim() : null;
961
+ const needsCopy = !fs.existsSync(dstProfile) || (lastProfile && lastProfile !== profileDir);
962
+ if (needsCopy && fs.existsSync(srcProfile)) {
963
+ // Clean old profile if switching
964
+ if (lastProfile && lastProfile !== profileDir) {
965
+ const oldDst = path.join(cdpDataDir, lastProfile);
966
+ if (fs.existsSync(oldDst)) fs.rmSync(oldDst, { recursive: true, force: true });
967
+ }
956
968
  fs.mkdirSync(cdpDataDir, { recursive: true });
957
969
  execFileSync("cp", ["-R", srcProfile, dstProfile], { timeout: 30000, stdio: "ignore" });
970
+ fs.writeFileSync(profileMarker, profileDir);
958
971
  // Create minimal Local State with single profile to avoid profile picker
959
972
  const localStateSrc = path.join(chromeUserDataDir, "Local State");
960
973
  if (fs.existsSync(localStateSrc)) {
@@ -986,7 +999,7 @@ async function ensureCdpAvailable() {
986
999
  } catch {
987
1000
  return {
988
1001
  available: false,
989
- reason: `Chrome 자동 실행에 실패했습니다. Chrome 수동으로 --remote-debugging-port=9222 --user-data-dir=~/.chrome-cdp 옵션과 함께 실행해주세요.`,
1002
+ reason: `Failed to auto-launch Chrome. Manually run Chrome with --remote-debugging-port=9222 --user-data-dir=~/.chrome-cdp.`,
990
1003
  };
991
1004
  }
992
1005
 
@@ -1005,20 +1018,20 @@ async function ensureCdpAvailable() {
1005
1018
 
1006
1019
  return {
1007
1020
  available: false,
1008
- reason: "Chrome 실행했지만 CDP에 연결할 없습니다. Chrome을 완전히 종료한 다시 시도해주세요. (이미 실행 중인 Chrome CDP 없이 시작된 경우 재시작 필요)",
1021
+ reason: "Chrome launched but cannot connect to CDP. Fully close Chrome and try again. (Restart required if Chrome was started without CDP)",
1009
1022
  };
1010
1023
  }
1011
1024
 
1012
1025
  // Unreachable (all platforms handled above), but keep as safety net
1013
1026
  return {
1014
1027
  available: false,
1015
- reason: "Chrome CDP 활성화할 없습니다. Chrome을 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
1028
+ reason: "Cannot activate Chrome CDP. Run Chrome with --remote-debugging-port=9222.",
1016
1029
  };
1017
1030
  }
1018
1031
 
1019
1032
  function collectBrowserLlmTabsViaAppleScript() {
1020
1033
  if (process.platform !== "darwin") {
1021
- return { tabs: [], note: "AppleScript 스캔은 macOS에서만 지원됩니다." };
1034
+ return { tabs: [], note: "AppleScript tab scanning is only supported on macOS." };
1022
1035
  }
1023
1036
 
1024
1037
  const escapedDomains = DEFAULT_LLM_DOMAINS.map(d => d.replace(/"/g, '\\"'));
@@ -1104,14 +1117,14 @@ return outText`;
1104
1117
  return {
1105
1118
  tabs,
1106
1119
  note: errors.length > 0
1107
- ? `일부 브라우저 접근 실패: ${errors.map(e => `${e.browser} (${e.url})`).join(", ")}`
1120
+ ? `Some browser access failed: ${errors.map(e => `${e.browser} (${e.url})`).join(", ")}`
1108
1121
  : null,
1109
1122
  };
1110
1123
  } catch (error) {
1111
1124
  const reason = error instanceof Error ? error.message : "unknown error";
1112
1125
  return {
1113
1126
  tabs: [],
1114
- note: `브라우저 스캔 실패: ${reason}. macOS 자동화 권한(터미널 -> 브라우저 제어)을 확인하세요.`,
1127
+ note: `Browser tab scan failed: ${reason}. Check macOS automation permissions (Terminal -> Browser control).`,
1115
1128
  };
1116
1129
  }
1117
1130
  }
@@ -1128,7 +1141,7 @@ async function collectBrowserLlmTabs() {
1128
1141
  if (mode === "off") {
1129
1142
  return {
1130
1143
  tabs: dedupeBrowserTabs(tabs),
1131
- note: notes.length > 0 ? notes.join(" | ") : "브라우저 자동 스캔이 비활성화되었습니다.",
1144
+ note: notes.length > 0 ? notes.join(" | ") : "Browser tab auto-scanning is disabled.",
1132
1145
  };
1133
1146
  }
1134
1147
 
@@ -1138,7 +1151,7 @@ async function collectBrowserLlmTabs() {
1138
1151
  tabs.push(...mac.tabs);
1139
1152
  if (mac.note) notes.push(mac.note);
1140
1153
  } else if (mode === "applescript" && process.platform !== "darwin") {
1141
- notes.push("AppleScript 스캔은 macOS 전용입니다. CDP 스캔으로 전환하세요.");
1154
+ notes.push("AppleScript scanning is macOS only. Switch to CDP scanning.");
1142
1155
  }
1143
1156
 
1144
1157
  const shouldUseCdp = mode === "auto" || mode === "cdp";
@@ -1211,7 +1224,7 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
1211
1224
  // Ensure CDP is available before probing browser tabs
1212
1225
  const cdpStatus = await ensureCdpAvailable();
1213
1226
  if (cdpStatus.launched) {
1214
- browserNote = "Chrome CDP 자동 실행됨 (--remote-debugging-port=9222)";
1227
+ browserNote = "Chrome CDP auto-launched (--remote-debugging-port=9222)";
1215
1228
  }
1216
1229
 
1217
1230
  const { tabs, note } = await collectBrowserLlmTabs();
@@ -1345,28 +1358,28 @@ function formatSpeakerCandidatesReport({ candidates, browserNote }) {
1345
1358
  let out = "## Selectable Speakers\n\n";
1346
1359
  out += "### CLI\n";
1347
1360
  if (cli.length === 0) {
1348
- out += "- (감지된 로컬 CLI 없음)\n\n";
1361
+ out += "- (No local CLI detected)\n\n";
1349
1362
  } else {
1350
1363
  out += `${cli.map(c => {
1351
- const status = c.live === false ? " ❌ 실행 불가" : c.live === true ? " ✅ 실행 가능" : "";
1364
+ const status = c.live === false ? " ❌ not executable" : c.live === true ? " ✅ executable" : "";
1352
1365
  return `- \`${c.speaker}\` (command: ${c.command})${status}`;
1353
1366
  }).join("\n")}\n\n`;
1354
1367
  }
1355
1368
 
1356
- out += "### Browser LLM (감지됨)\n";
1369
+ out += "### Browser LLM (detected)\n";
1357
1370
  if (detected.length === 0) {
1358
- out += "- (브라우저에서 감지된 LLM 없음)\n";
1371
+ out += "- (No LLM tabs detected in browser)\n";
1359
1372
  } else {
1360
1373
  out += `${detected.map(c => {
1361
- const icon = c.cdp_available ? "⚡자동" : "📋클립보드";
1374
+ const icon = c.cdp_available ? "⚡auto" : "📋clipboard";
1362
1375
  const extTag = String(c.url || "").startsWith("chrome-extension://") ? " [Extension]" : "";
1363
1376
  return `- \`${c.speaker}\` [${icon}]${extTag} [${c.browser}] ${c.title}\n ${c.url}`;
1364
1377
  }).join("\n")}\n`;
1365
1378
  }
1366
1379
 
1367
- out += "\n### Web LLM (자동 등록)\n";
1380
+ out += "\n### Web LLM (auto-registered)\n";
1368
1381
  out += `${autoReg.map(c => {
1369
- const icon = c.cdp_available ? "⚡자동" : "📋클립보드";
1382
+ const icon = c.cdp_available ? "⚡auto" : "📋clipboard";
1370
1383
  return `- \`${c.speaker}\` [${icon}] — ${c.title} (${c.url})`;
1371
1384
  }).join("\n")}\n`;
1372
1385
 
@@ -1477,11 +1490,11 @@ function resolveTransportForSpeaker(state, speaker) {
1477
1490
 
1478
1491
  // CLI-specific invocation flags for non-interactive execution
1479
1492
  const CLI_INVOCATION_HINTS = {
1480
- claude: { cmd: "claude", flags: '-p --output-format text', example: 'CLAUDECODE= claude -p --output-format text "프롬프트"', envPrefix: 'CLAUDECODE=', modelFlag: '--model', provider: 'claude' },
1481
- codex: { cmd: "codex", flags: 'exec --model gpt-5.4-codex', example: 'codex exec --model gpt-5.4-codex "프롬프트"', modelFlag: '--model', defaultModel: 'gpt-5.4-codex', provider: 'chatgpt' },
1482
- gemini: { cmd: "gemini", flags: '', example: 'gemini "프롬프트"', modelFlag: '--model', provider: 'gemini' },
1483
- aider: { cmd: "aider", flags: '--message', example: 'aider --message "프롬프트"', modelFlag: '--model', provider: 'chatgpt' },
1484
- cursor: { cmd: "cursor", flags: '', example: 'cursor "프롬프트"', modelFlag: null, provider: 'chatgpt' },
1493
+ claude: { cmd: "claude", flags: '-p --output-format text', example: 'CLAUDECODE= claude -p --output-format text "prompt"', envPrefix: 'CLAUDECODE=', modelFlag: '--model', provider: 'claude' },
1494
+ codex: { cmd: "codex", flags: 'exec --model gpt-5.4-codex', example: 'codex exec --model gpt-5.4-codex "prompt"', modelFlag: '--model', defaultModel: 'gpt-5.4-codex', provider: 'chatgpt' },
1495
+ gemini: { cmd: "gemini", flags: '', example: 'gemini "prompt"', modelFlag: '--model', provider: 'gemini' },
1496
+ aider: { cmd: "aider", flags: '--message', example: 'aider --message "prompt"', modelFlag: '--model', provider: 'chatgpt' },
1497
+ cursor: { cmd: "cursor", flags: '', example: 'cursor "prompt"', modelFlag: null, provider: 'chatgpt' },
1485
1498
  };
1486
1499
 
1487
1500
  function formatTransportGuidance(transport, state, speaker) {
@@ -1493,23 +1506,23 @@ function formatTransportGuidance(transport, state, speaker) {
1493
1506
  let modelGuide = "";
1494
1507
  if (hint) {
1495
1508
  const prefix = hint.envPrefix || '';
1496
- invocationGuide = `\n\n**CLI 호출 방법:** \`${hint.example}\`\n(플래그: \`${prefix}${hint.cmd} ${hint.flags}\`)`;
1509
+ invocationGuide = `\n\n**CLI invocation:** \`${hint.example}\`\n(flags: \`${prefix}${hint.cmd} ${hint.flags}\`)`;
1497
1510
  if (hint.modelFlag && hint.provider) {
1498
1511
  const cliModel = getModelSelectionForTurn(state, speaker, hint.provider);
1499
1512
  if (cliModel.model !== 'default') {
1500
- modelGuide = `\n**추천 모델:** ${cliModel.model} (${cliModel.reason})\n**모델 플래그:** \`${hint.modelFlag} ${cliModel.model}\``;
1513
+ modelGuide = `\n**Recommended model:** ${cliModel.model} (${cliModel.reason})\n**Model flag:** \`${hint.modelFlag} ${cliModel.model}\``;
1501
1514
  }
1502
1515
  }
1503
1516
  }
1504
- return `CLI speaker입니다. \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`로 직접 응답하세요.${invocationGuide}${modelGuide}\n\n⛔ **API 호출 금지**: REST API, HTTP 요청, urllib, requests 등으로 LLM API를 직접 호출하지 마세요. 반드시 위 CLI 도구만 사용하세요.`;
1517
+ return `CLI speaker. Respond directly via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`.${invocationGuide}${modelGuide}\n\n⛔ **No API calls**: Do not call LLM APIs directly via REST API, HTTP requests, urllib, requests, etc. Only use the CLI tools above.`;
1505
1518
  }
1506
1519
  case "clipboard":
1507
- return `브라우저 LLM speaker입니다. CDP 자동 연결 시도 중... Chrome이 이미 CDP 없이 실행 중이면 재시작이 필요할 수 있습니다.\n\n⛔ **API 호출 금지**: speaker 브라우저로만 응답합니다. REST API, HTTP 요청으로 LLM을 호출하지 마세요.`;
1520
+ return `Browser LLM speaker. Attempting CDP auto-connect... Chrome may need to be restarted if already running without CDP.\n\n⛔ **No API calls**: This speaker responds only via web browser. Do not call LLMs via REST API or HTTP requests.`;
1508
1521
  case "browser_auto":
1509
- return `자동 브라우저 speaker입니다. \`deliberation_browser_auto_turn(session_id: "${sid}")\`으로 자동 진행됩니다. CDP를 통해 브라우저 LLM 직접 입력하고 응답을 읽습니다.\n\n⛔ **API 호출 금지**: CDP 자동화로만 진행합니다. REST API, HTTP 요청 사용 금지.`;
1522
+ return `Auto browser speaker. Proceed automatically with \`deliberation_browser_auto_turn(session_id: "${sid}")\`. Inputs directly to browser LLM via CDP and reads responses.\n\n⛔ **No API calls**: Proceeds only via CDP automation. No REST API or HTTP requests.`;
1510
1523
  case "manual":
1511
1524
  default:
1512
- return `수동 speaker입니다. 해당 LLM **웹 UI 또는 CLI 도구**를 통해 응답을 받아 \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`로 제출하세요.\n\n⛔ **API 호출 절대 금지**: REST API, HTTP 요청(urllib, requests, fetch )으로 LLM API를 직접 호출하는 것은 금지됩니다. 반드시 웹 브라우저 UI 또는 CLI 도구만 사용하세요. API 키로 직접 호출하면 deliberation 참여가 거부됩니다.`;
1525
+ return `Manual speaker. Get a response from the LLM's **web UI or CLI tool** and submit via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`.\n\n⛔ **Absolutely no API calls**: Calling LLM APIs directly via REST API, HTTP requests (urllib, requests, fetch, etc.) is forbidden. Only use web browser UI or CLI tools. Direct API key calls will result in deliberation participation being rejected.`;
1513
1526
  }
1514
1527
  }
1515
1528
 
@@ -1641,7 +1654,7 @@ function readContextFromDirs(dirs, maxChars = 15000) {
1641
1654
  }
1642
1655
  }
1643
1656
  }
1644
- return context || "(컨텍스트 파일을 찾을 수 없습니다)";
1657
+ return context || "(No context files found)";
1645
1658
  }
1646
1659
 
1647
1660
  // ── State helpers ──────────────────────────────────────────────
@@ -1681,15 +1694,15 @@ function listActiveSessions() {
1681
1694
  }
1682
1695
 
1683
1696
  function resolveSessionId(sessionId) {
1684
- // session_id 주어지면 그대로 사용
1697
+ // Use session_id directly if provided
1685
1698
  if (sessionId) return sessionId;
1686
1699
 
1687
- // 없으면 활성 세션이 1개일 자동 선택
1700
+ // Auto-select when only one active session
1688
1701
  const active = listActiveSessions();
1689
1702
  if (active.length === 0) return null;
1690
1703
  if (active.length === 1) return active[0].id;
1691
1704
 
1692
- // 여러 개면 null (목록 표시 필요)
1705
+ // null if multiple (need to show list)
1693
1706
  return "MULTIPLE";
1694
1707
  }
1695
1708
 
@@ -1775,7 +1788,7 @@ const MONITOR_SCRIPT = path.join(INSTALL_DIR, "session-monitor.sh");
1775
1788
  const MONITOR_SCRIPT_WIN = path.join(INSTALL_DIR, "session-monitor-win.js");
1776
1789
 
1777
1790
  function tmuxWindowName(sessionId) {
1778
- // tmux 윈도우 이름은 짧게 (마지막 부분 제거하고 20)
1791
+ // Keep tmux window name short (remove last part, 20 chars)
1779
1792
  return sessionId.replace(/[^a-zA-Z0-9가-힣-]/g, "").slice(0, 25);
1780
1793
  }
1781
1794
 
@@ -2243,13 +2256,13 @@ function closeAllMonitorTerminals() {
2243
2256
  function multipleSessionsError() {
2244
2257
  const active = listActiveSessions();
2245
2258
  const list = active.map(s => `- **${s.id}**: "${s.topic}" (Round ${s.current_round}/${s.max_rounds}, next: ${s.current_speaker})`).join("\n");
2246
- return `여러 활성 세션이 있습니다. session_id를 지정하세요:\n\n${list}`;
2259
+ return t(`Multiple active sessions found. Please specify session_id:\n\n${list}`, `여러 활성 세션이 있습니다. session_id를 지정하세요:\n\n${list}`, "en");
2247
2260
  }
2248
2261
 
2249
2262
  function formatRecentLogForPrompt(state, maxEntries = 4) {
2250
2263
  const entries = Array.isArray(state.log) ? state.log.slice(-Math.max(0, maxEntries)) : [];
2251
2264
  if (entries.length === 0) {
2252
- return "(아직 이전 응답 없음)";
2265
+ return "(No previous responses yet)";
2253
2266
  }
2254
2267
  return entries.map(e => {
2255
2268
  const content = String(e.content || "").trim();
@@ -2259,7 +2272,7 @@ function formatRecentLogForPrompt(state, maxEntries = 4) {
2259
2272
 
2260
2273
  function buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries = 4) {
2261
2274
  const recent = formatRecentLogForPrompt(state, includeHistoryEntries);
2262
- const extraPrompt = prompt ? `\n[추가 지시]\n${prompt}\n` : "";
2275
+ const extraPrompt = prompt ? `\n[Additional instructions]\n${prompt}\n` : "";
2263
2276
 
2264
2277
  // Role prompt injection
2265
2278
  const speakerRole = (state.speaker_roles || {})[speaker] || "free";
@@ -2281,9 +2294,9 @@ ${recent}
2281
2294
  [/recent_log]${extraPrompt}
2282
2295
 
2283
2296
  [response_rule]
2284
- - 토론 맥락을 반영해 ${speaker} 이번 응답만 작성
2285
- - 마크다운 본문만 출력 (불필요한 머리말/꼬리말 금지)${speakerRole !== "free" ? `\n- 배정된 역할(${speakerRole}) 관점에서 분석하고 응답` : ""}
2286
- - 응답 마지막에 반드시 [AGREE], [DISAGREE], 또는 [CONDITIONAL: 사유] 하나를 포함
2297
+ - Write only ${speaker}'s response for this turn reflecting the discussion context above
2298
+ - Output markdown body only (no unnecessary headers/footers)${speakerRole !== "free" ? `\n- Analyze and respond from the perspective of assigned role (${speakerRole})` : ""}
2299
+ - Must include one of [AGREE], [DISAGREE], or [CONDITIONAL: reason] at the end of response
2287
2300
  [/response_rule]
2288
2301
  [/deliberation_turn_request]
2289
2302
  `;
@@ -2292,7 +2305,7 @@ ${recent}
2292
2305
  function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel_used, fallback_reason }) {
2293
2306
  const resolved = resolveSessionId(session_id);
2294
2307
  if (!resolved) {
2295
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
2308
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
2296
2309
  }
2297
2310
  if (resolved === "MULTIPLE") {
2298
2311
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2301,12 +2314,12 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2301
2314
  return withSessionLock(resolved, () => {
2302
2315
  const state = loadSession(resolved);
2303
2316
  if (!state || state.status !== "active") {
2304
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
2317
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
2305
2318
  }
2306
2319
 
2307
2320
  const normalizedSpeaker = normalizeSpeaker(speaker);
2308
2321
  if (!normalizedSpeaker) {
2309
- return { content: [{ type: "text", text: "speaker 값이 비어 있습니다. 응답자 이름을 지정하세요." }] };
2322
+ return { content: [{ type: "text", text: t("Speaker value is empty. Please specify a speaker name.", "speaker 값이 비어 있습니다. 응답자 이름을 지정하세요.", "en") }] };
2310
2323
  }
2311
2324
 
2312
2325
  state.speakers = buildSpeakerOrder(state.speakers, state.current_speaker, "end");
@@ -2321,17 +2334,17 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2321
2334
  return {
2322
2335
  content: [{
2323
2336
  type: "text",
2324
- text: `[${state.id}] 지금은 **${state.current_speaker}** 차례입니다. ${normalizedSpeaker}는 대기하세요.`,
2337
+ text: t(`[${state.id}] It is currently **${state.current_speaker}**'s turn. ${normalizedSpeaker} please wait.`, `[${state.id}] 지금은 **${state.current_speaker}** 차례입니다. ${normalizedSpeaker}는 대기하세요.`, state?.lang),
2325
2338
  }],
2326
2339
  };
2327
2340
  }
2328
2341
 
2329
- // turn_id 검증 (선택적제공 반드시 일치해야 함)
2342
+ // turn_id validation (optionalmust match if provided)
2330
2343
  if (turn_id && state.pending_turn_id && turn_id !== state.pending_turn_id) {
2331
2344
  return {
2332
2345
  content: [{
2333
2346
  type: "text",
2334
- text: `[${state.id}] turn_id 불일치. 예상: "${state.pending_turn_id}", 수신: "${turn_id}". 오래된 요청이거나 중복 제출일 수 있습니다.`,
2347
+ text: t(`[${state.id}] turn_id mismatch. Expected: "${state.pending_turn_id}", received: "${turn_id}". May be a stale request or duplicate submission.`, `[${state.id}] turn_id 불일치. 예상: "${state.pending_turn_id}", 수신: "${turn_id}". 오래된 요청이거나 중복 제출일 수 있습니다.`, state?.lang),
2335
2348
  }],
2336
2349
  };
2337
2350
  }
@@ -2372,7 +2385,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2372
2385
  return {
2373
2386
  content: [{
2374
2387
  type: "text",
2375
- text: `✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} 완료. Forum 업데이트됨 (${state.log.length}건 응답 축적).\n\n🏁 **모든 라운드 종료!**\ndeliberation_synthesize(session_id: "${state.id}")로 합성 보고서를 작성하세요.`,
2388
+ text: t(`✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} complete. Forum updated (${state.log.length} responses accumulated).\n\n🏁 **All rounds complete!**\nCreate a synthesis report with deliberation_synthesize(session_id: "${state.id}").`, `✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} 완료. Forum 업데이트됨 (${state.log.length}건 응답 축적).\n\n🏁 **모든 라운드 종료!**\ndeliberation_synthesize(session_id: "${state.id}")로 합성 보고서를 작성하세요.`, state?.lang),
2376
2389
  }],
2377
2390
  };
2378
2391
  }
@@ -2387,7 +2400,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2387
2400
  return {
2388
2401
  content: [{
2389
2402
  type: "text",
2390
- text: `✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} 완료. Forum 업데이트됨 (${state.log.length}건 응답 축적).\n\n**다음:** ${state.current_speaker} (Round ${state.current_round})`,
2403
+ text: t(`✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} complete. Forum updated (${state.log.length} responses accumulated).\n\n**Next:** ${state.current_speaker} (Round ${state.current_round})`, `✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} 완료. Forum 업데이트됨 (${state.log.length}건 응답 축적).\n\n**다음:** ${state.current_speaker} (Round ${state.current_round})`, state?.lang),
2391
2404
  }],
2392
2405
  };
2393
2406
  });
@@ -2426,11 +2439,11 @@ const server = new McpServer({
2426
2439
 
2427
2440
  server.tool(
2428
2441
  "deliberation_start",
2429
- " deliberation을 시작합니다. 여러 토론을 동시에 진행할 있습니다.",
2442
+ "Start a new deliberation. Multiple deliberations can run simultaneously.",
2430
2443
  {
2431
- topic: z.string().describe("토론 주제"),
2432
- rounds: z.coerce.number().optional().describe("라운드 (미지정 config 설정 따름, 기본 3)"),
2433
- first_speaker: z.string().trim().min(1).max(64).optional().describe(" 발언자 이름 (미지정 speakers의 항목)"),
2444
+ topic: z.string().describe("Discussion topic"),
2445
+ rounds: z.coerce.number().optional().describe("Number of rounds (defaults to config setting, default 3)"),
2446
+ first_speaker: z.string().trim().min(1).max(64).optional().describe("First speaker name (defaults to first item in speakers)"),
2434
2447
  speakers: z.preprocess(
2435
2448
  (v) => {
2436
2449
  const parsed = typeof v === "string" ? JSON.parse(v) : v;
@@ -2439,31 +2452,31 @@ server.tool(
2439
2452
  return parsed.map(item => (typeof item === "object" && item !== null && item.name) ? item.name : item);
2440
2453
  },
2441
2454
  z.array(z.string().trim().min(1).max(64)).min(1).optional()
2442
- ).describe("참가자 이름 목록. 문자열 배열 또는 {name, role, instructions} 객체 배열 모두 지원"),
2455
+ ).describe("Participant name list. Supports both string arrays and {name, role, instructions} object arrays"),
2443
2456
  speaker_instructions: z.preprocess(
2444
2457
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
2445
2458
  z.record(z.string(), z.string()).optional()
2446
- ).describe("speaker 추가 지시사항 (예: {\"claude\": \"비판적으로 검토\"})"),
2459
+ ).describe("Per-speaker additional instructions (e.g., {\"claude\": \"review critically\"})"),
2447
2460
  require_manual_speakers: z.preprocess(
2448
2461
  (v) => (typeof v === "string" ? v === "true" : v),
2449
2462
  z.boolean().optional()
2450
- ).describe("true speakers 반드시 직접 지정해야 시작 (미지정 config 설정 따름)"),
2463
+ ).describe("If true, speakers must be explicitly specified to start (defaults to config setting)"),
2451
2464
  auto_discover_speakers: z.preprocess(
2452
2465
  (v) => (typeof v === "string" ? v === "true" : v),
2453
2466
  z.boolean().optional()
2454
- ).describe("speakers 생략 자동 탐색 여부 (미지정 config 설정 따름)"),
2467
+ ).describe("Whether to auto-discover speakers when omitted (defaults to config setting)"),
2455
2468
  participant_types: z.preprocess(
2456
2469
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
2457
2470
  z.record(z.string(), z.enum(["cli", "browser", "browser_auto", "manual"])).optional()
2458
- ).describe("speaker 타입 오버라이드 (예: {\"chatgpt\": \"browser_auto\"})"),
2471
+ ).describe("Per-speaker type override (e.g., {\"chatgpt\": \"browser_auto\"})"),
2459
2472
  ordering_strategy: z.enum(["auto", "cyclic", "random", "weighted-random"]).optional()
2460
- .describe("발언 순서 전략: auto(스피커 수에 따라 자동), cyclic(순서대로), random(매턴 무작위), weighted-random( 말한 사람 우선)"),
2473
+ .describe("Ordering strategy: auto (automatic based on speaker count), cyclic (sequential), random (random each turn), weighted-random (less spoken speakers first)"),
2461
2474
  speaker_roles: z.preprocess(
2462
2475
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
2463
2476
  z.record(z.string(), z.enum(["critic", "implementer", "mediator", "researcher", "free"])).optional()
2464
- ).describe("speaker 역할 배정 (예: {\"claude\": \"critic\", \"codex\": \"implementer\"})"),
2477
+ ).describe("Per-speaker role assignment (e.g., {\"claude\": \"critic\", \"codex\": \"implementer\"})"),
2465
2478
  role_preset: z.enum(["balanced", "debate", "research", "brainstorm", "review", "consensus"]).optional()
2466
- .describe("역할 프리셋 (balanced/debate/research/brainstorm/review/consensus). speaker_roles 명시되면 무시됨"),
2479
+ .describe("Role preset (balanced/debate/research/brainstorm/review/consensus). Ignored if speaker_roles is specified"),
2467
2480
  },
2468
2481
  safeToolHandler("deliberation_start", async ({ topic, rounds, first_speaker, speakers, speaker_instructions, require_manual_speakers, auto_discover_speakers, participant_types, ordering_strategy, speaker_roles, role_preset }) => {
2469
2482
  // ── First-time onboarding guard ──
@@ -2474,7 +2487,7 @@ server.tool(
2474
2487
  return {
2475
2488
  content: [{
2476
2489
  type: "text",
2477
- text: `🎉 **Deliberation 사용을 환영합니다!**\n\n시작 전에 기본 설정을 해주세요.\n\n**현재 감지된 스피커:**\n${candidateText}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n아래 설정을 번에 지정할 있습니다:\n\n\`\`\`\ndeliberation_cli_config(\n require_speaker_selection: true/false,\n default_rounds: 3,\n default_ordering: "auto"\n)\n\`\`\`\n\n**1. 스피커 참여 모드** (\`require_speaker_selection\`)\n - \`true\` — 매번 참여할 스피커를 직접 선택\n - \`false\` — 감지된 CLI + 브라우저 LLM 전부 자동 참여\n\n**2. 기본 라운드 수** (\`default_rounds\`)\n - \`1\` — 빠른 의견 수렴\n - \`3\` — 기본 (권장)\n - \`5\` — 심층 토론\n\n**3. 발언 순서 전략** (\`default_ordering\`)\n - \`"auto"\` — 2명이면 cyclic, 3명 이상이면 weighted-random (권장)\n - \`"cyclic"\` — 고정 순서\n - \`"random"\` — 매턴 무작위\n - \`"weighted-random"\` — 발언한 사람 우선`,
2490
+ text: `🎉 **Welcome to Deliberation!**\n\nPlease configure basic settings before starting.\n\n**Currently detected speakers:**\n${candidateText}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nYou can set all options at once:\n\n\`\`\`\ndeliberation_cli_config(\n require_speaker_selection: true/false,\n default_rounds: 3,\n default_ordering: "auto"\n)\n\`\`\`\n\n**1. Speaker participation mode** (\`require_speaker_selection\`)\n - \`true\` — Select participating speakers each time\n - \`false\` — All detected CLI + browser LLMs auto-join\n\n**2. Default rounds** (\`default_rounds\`)\n - \`1\` — Quick consensus\n - \`3\` — Default (recommended)\n - \`5\` — Deep discussion\n\n**3. Ordering strategy** (\`default_ordering\`)\n - \`"auto"\` — cyclic for 2 speakers, weighted-random for 3+ (recommended)\n - \`"cyclic"\` — Fixed order\n - \`"random"\` — Random each turn\n - \`"weighted-random"\` — Less spoken speakers first`,
2478
2491
  }],
2479
2492
  };
2480
2493
  }
@@ -2501,15 +2514,15 @@ server.tool(
2501
2514
  if (!hasManualSpeakers && effectiveRequireManual) {
2502
2515
  const candidateText = formatSpeakerCandidatesReport(candidateSnapshot);
2503
2516
  const llmSuggested = Array.isArray(speakers) && speakers.length > 0
2504
- ? `\n\n💡 **LLM 제안한 스피커:** ${speakers.join(", ")}\n위 제안을 사용하려면 \`require_manual_speakers: true\`와 함께 speakers를 다시 전달하세요.`
2517
+ ? `\n\n💡 **LLM suggested speakers:** ${speakers.join(", ")}\nTo use this suggestion, pass speakers again with \`require_manual_speakers: true\`.`
2505
2518
  : "";
2506
2519
  const configNote = configRequiresSelection
2507
- ? "\n\n⚙️ `require_speaker_selection: true` 설정에 의해 사용자가 직접 스피커를 선택해야 합니다."
2520
+ ? "\n\n⚙️ `require_speaker_selection: true` setting requires you to manually select speakers."
2508
2521
  : "";
2509
2522
  return {
2510
2523
  content: [{
2511
2524
  type: "text",
2512
- text: `스피커를 직접 선택해야 deliberation을 시작할 있습니다.${configNote}${llmSuggested}\n\n${candidateText}\n\n예시:\n\ndeliberation_start(\n topic: "${topic.replace(/"/g, '\\"')}",\n rounds: ${rounds},\n speakers: ["codex", "web-claude-1", "web-chatgpt-1"],\n require_manual_speakers: true,\n first_speaker: "codex"\n)\n\n먼저 deliberation_speaker_candidates 호출해 현재 선택 가능한 스피커를 확인하세요.`,
2525
+ text: `Speakers must be manually selected to start a deliberation.${configNote}${llmSuggested}\n\n${candidateText}\n\nExample:\n\ndeliberation_start(\n topic: "${topic.replace(/"/g, '\\"')}",\n rounds: ${rounds},\n speakers: ["codex", "web-claude-1", "web-chatgpt-1"],\n require_manual_speakers: true,\n first_speaker: "codex"\n)\n\nFirst call deliberation_speaker_candidates to check currently available speakers.`,
2513
2526
  }],
2514
2527
  };
2515
2528
  }
@@ -2553,7 +2566,7 @@ server.tool(
2553
2566
  return {
2554
2567
  content: [{
2555
2568
  type: "text",
2556
- text: `⚠️ Deliberation에는 최소 2명의 speaker가 필요합니다. 현재 ${speakerOrder.length}명만 지정됨: ${speakerOrder.join(", ")}\n\n사용 가능한 스피커 후보:\n${candidateText}\n\n예시:\ndeliberation_start(topic: "${topic.slice(0, 50)}...", speakers: ["claude", "codex", "web-gemini-1"])`,
2569
+ text: `⚠️ Deliberation requires at least 2 speakers. Currently only ${speakerOrder.length} specified: ${speakerOrder.join(", ")}\n\nAvailable speaker candidates:\n${candidateText}\n\nExample:\ndeliberation_start(topic: "${topic.slice(0, 50)}...", speakers: ["claude", "codex", "web-gemini-1"])`,
2557
2570
  }],
2558
2571
  };
2559
2572
  }
@@ -2570,12 +2583,12 @@ server.tool(
2570
2583
  // cli_auto_turn will handle runtime errors per-turn.
2571
2584
  let detectWarningLiveness = "";
2572
2585
  if (nonLiveCli.length > 0) {
2573
- detectWarningLiveness = `\n\n⚠️ 일부 CLI가 현재 실행 불가 상태이지만 사용자 선택을 존중하여 진행합니다:\n${nonLiveCli.map(s => ` - \`${s}\` ❌`).join("\n")}\n턴 진행 CLI 실행을 재시도합니다. 실패 해당 턴에서 오류가 보고됩니다.`;
2586
+ detectWarningLiveness = `\n\n⚠️ Some CLIs are currently not executable but proceeding per user selection:\n${nonLiveCli.map(s => ` - \`${s}\` ❌`).join("\n")}\nCLI execution will be retried during turns. Errors will be reported on failure.`;
2574
2587
  }
2575
2588
 
2576
2589
  const participantMode = hasManualSpeakers
2577
- ? "수동 지정"
2578
- : (autoDiscoveredSpeakers.length > 0 ? "자동 탐색(PATH)" : "기본값");
2590
+ ? "manually specified"
2591
+ : (autoDiscoveredSpeakers.length > 0 ? "auto-discovered (PATH)" : "default");
2579
2592
 
2580
2593
  const degradationLevels = await detectDegradationLevels();
2581
2594
 
@@ -2583,6 +2596,7 @@ server.tool(
2583
2596
  id: sessionId,
2584
2597
  project: getProjectSlug(),
2585
2598
  topic,
2599
+ lang: detectLang(topic),
2586
2600
  status: "active",
2587
2601
  max_rounds: rounds,
2588
2602
  current_round: 1,
@@ -2610,7 +2624,7 @@ server.tool(
2610
2624
  return {
2611
2625
  content: [{
2612
2626
  type: "text",
2613
- text: `❌ 브라우저 LLM speaker가 포함되어 있지만 CDP에 연결할 없습니다.\n\n${cdpReady.reason}\n\nCDP 연결 다시 deliberation_start를 호출하세요.`,
2627
+ text: `❌ Browser LLM speakers included but cannot connect to CDP.\n\n${cdpReady.reason}\n\nCall deliberation_start again after establishing CDP connection.`,
2614
2628
  }],
2615
2629
  };
2616
2630
  }
@@ -2641,20 +2655,20 @@ server.tool(
2641
2655
  const isWin = process.platform === "win32";
2642
2656
  const terminalMsg = !tmuxOpened
2643
2657
  ? isWin
2644
- ? `\n⚠️ Windows Terminal 찾을 없어 모니터 터미널 미생성`
2645
- : `\n⚠️ tmux 찾을 없어 모니터 터미널 미생성`
2658
+ ? `\n⚠️ Windows Terminal not found, monitor terminal not created`
2659
+ : `\n⚠️ tmux not found, monitor terminal not created`
2646
2660
  : physicalOpened
2647
2661
  ? isWin
2648
- ? `\n🖥️ 모니터 터미널 오픈됨 (Windows Terminal)`
2649
- : `\n🖥️ 모니터 터미널 오픈됨: tmux new-session -t ${TMUX_SESSION}`
2662
+ ? `\n🖥️ Monitor terminal opened (Windows Terminal)`
2663
+ : `\n🖥️ Monitor terminal opened: tmux new-session -t ${TMUX_SESSION}`
2650
2664
  : isWin
2651
- ? `\n⚠️ 모니터 터미널 자동 오픈 실패`
2652
- : `\n⚠️ tmux 윈도우는 생성됐지만 외부 터미널 자동 오픈 실패. 수동 실행: tmux new-session -t ${TMUX_SESSION}`;
2665
+ ? `\n⚠️ Monitor terminal auto-open failed`
2666
+ : `\n⚠️ tmux window created but external terminal auto-open failed. Manual run: tmux new-session -t ${TMUX_SESSION}`;
2653
2667
  const manualNotDetected = hasManualSpeakers
2654
2668
  ? speakerOrder.filter(s => !candidateSnapshot.candidates.some(c => c.speaker === s))
2655
2669
  : [];
2656
2670
  const detectWarning = manualNotDetected.length > 0
2657
- ? `\n\n⚠️ 현재 환경에서 즉시 검출되지 않은 speaker: ${manualNotDetected.join(", ")}\n(수동 지정으로는 참가 가능)`
2671
+ ? `\n\n⚠️ Speakers not immediately detected in current environment: ${manualNotDetected.join(", ")}\n(Can still participate via manual specification)`
2658
2672
  : "";
2659
2673
 
2660
2674
  const transportSummary = state.participant_profiles.map(p => {
@@ -2666,7 +2680,7 @@ server.tool(
2666
2680
  return {
2667
2681
  content: [{
2668
2682
  type: "text",
2669
- text: `✅ Deliberation 시작! Forum 생성되었습니다.\n\n**세션:** ${sessionId}\n**프로젝트:** ${state.project}\n**주제:** ${topic}\n**라운드:** ${rounds}\n**발언 순서:** ${state.ordering_strategy || "cyclic"}\n**참가자 구성:** ${participantMode}\n**참가자:** ${speakerOrder.join(", ")}\n**첫 발언:** ${state.current_speaker}\n**동시 진행 세션:** ${active.length}개${terminalMsg}${detectWarning}${detectWarningLiveness}\n\n**역할 배정:**${role_preset ? ` (프리셋: ${role_preset})` : ""}\n${speakerOrder.map(s => ` - \`${s}\`: ${(state.speaker_roles || {})[s] || "free"}`).join("\n")}\n\n**환경 상태:**\n${formatDegradationReport(state.degradation)}\n\n**Transport 라우팅:**\n${transportSummary}\n\n💡 이후 도구 호출 시 session_id: "${sessionId}" 사용하세요.\n📋 Forum 상태 조회: \`deliberation_status(session_id: "${sessionId}")\``,
2683
+ text: `✅ Deliberation started! Forum created.\n\n**Session:** ${sessionId}\n**Project:** ${state.project}\n**Topic:** ${topic}\n**Rounds:** ${rounds}\n**Ordering:** ${state.ordering_strategy || "cyclic"}\n**Participant mode:** ${participantMode}\n**Participants:** ${speakerOrder.join(", ")}\n**First speaker:** ${state.current_speaker}\n**Concurrent sessions:** ${active.length}${terminalMsg}${detectWarning}${detectWarningLiveness}\n\n**Role assignments:**${role_preset ? ` (preset: ${role_preset})` : ""}\n${speakerOrder.map(s => ` - \`${s}\`: ${(state.speaker_roles || {})[s] || "free"}`).join("\n")}\n\n**Environment status:**\n${formatDegradationReport(state.degradation)}\n\n**Transport routing:**\n${transportSummary}\n\n💡 Use session_id: "${sessionId}" for subsequent tool calls.\n📋 Check forum status: \`deliberation_status(session_id: "${sessionId}")\``,
2670
2684
  }],
2671
2685
  };
2672
2686
  })
@@ -2674,10 +2688,10 @@ server.tool(
2674
2688
 
2675
2689
  server.tool(
2676
2690
  "deliberation_speaker_candidates",
2677
- "사용자가 선택 가능한 스피커 후보(로컬 CLI + 브라우저 LLM )를 조회합니다.",
2691
+ "Query available speaker candidates (local CLI + browser LLM tabs).",
2678
2692
  {
2679
- include_cli: z.boolean().default(true).describe("로컬 CLI 후보 포함"),
2680
- include_browser: z.boolean().default(true).describe("브라우저 LLM 후보 포함"),
2693
+ include_cli: z.boolean().default(true).describe("Include local CLI candidates"),
2694
+ include_browser: z.boolean().default(true).describe("Include browser LLM tab candidates"),
2681
2695
  },
2682
2696
  async ({ include_cli, include_browser }) => {
2683
2697
  const snapshot = await collectSpeakerCandidates({ include_cli, include_browser });
@@ -2688,17 +2702,17 @@ server.tool(
2688
2702
 
2689
2703
  server.tool(
2690
2704
  "deliberation_list_active",
2691
- "현재 프로젝트에서 진행 중인 모든 deliberation 세션 목록을 반환합니다.",
2705
+ "List all active deliberation sessions in the current project.",
2692
2706
  {},
2693
2707
  async () => {
2694
2708
  const active = listActiveSessions();
2695
2709
  if (active.length === 0) {
2696
- return { content: [{ type: "text", text: "진행 중인 deliberation이 없습니다." }] };
2710
+ return { content: [{ type: "text", text: t("No active deliberations.", "진행 중인 deliberation이 없습니다.", "en") }] };
2697
2711
  }
2698
2712
 
2699
- let list = `## 진행 중인 Deliberation (${getProjectSlug()}) — ${active.length}개\n\n`;
2713
+ let list = `## Active Deliberations (${getProjectSlug()}) — ${active.length}\n\n`;
2700
2714
  for (const s of active) {
2701
- list += `### ${s.id}\n- **주제:** ${s.topic}\n- **상태:** ${s.status} | Round ${s.current_round}/${s.max_rounds} | Next: ${s.current_speaker}\n- **응답 수:** ${s.log.length}\n\n`;
2715
+ list += `### ${s.id}\n- **Topic:** ${s.topic}\n- **Status:** ${s.status} | Round ${s.current_round}/${s.max_rounds} | Next: ${s.current_speaker}\n- **Responses:** ${s.log.length}\n\n`;
2702
2716
  }
2703
2717
  return { content: [{ type: "text", text: list }] };
2704
2718
  }
@@ -2706,14 +2720,14 @@ server.tool(
2706
2720
 
2707
2721
  server.tool(
2708
2722
  "deliberation_status",
2709
- "deliberation 상태를 조회합니다. 활성 세션이 1개면 자동 선택, 여러 개면 session_id 필요.",
2723
+ "Query deliberation status. Auto-selects if only one active session, requires session_id for multiple.",
2710
2724
  {
2711
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
2725
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
2712
2726
  },
2713
2727
  async ({ session_id }) => {
2714
2728
  const resolved = resolveSessionId(session_id);
2715
2729
  if (!resolved) {
2716
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다. deliberation_start로 시작하세요." }] };
2730
+ return { content: [{ type: "text", text: t("No active deliberation. Start one with deliberation_start.", "활성 deliberation이 없습니다. deliberation_start로 시작하세요.", "en") }] };
2717
2731
  }
2718
2732
  if (resolved === "MULTIPLE") {
2719
2733
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2721,13 +2735,13 @@ server.tool(
2721
2735
 
2722
2736
  const state = loadSession(resolved);
2723
2737
  if (!state) {
2724
- return { content: [{ type: "text", text: `세션 "${resolved}"을 찾을 수 없습니다.` }] };
2738
+ return { content: [{ type: "text", text: t(`Session "${resolved}" not found.`, `세션 "${resolved}"을 찾을 수 없습니다.`, "en") }] };
2725
2739
  }
2726
2740
 
2727
2741
  return {
2728
2742
  content: [{
2729
2743
  type: "text",
2730
- text: `📋 **Forum 현황** — ${state.id}\n\n**프로젝트:** ${state.project}\n**주제:** ${state.topic}\n**상태:** ${state.status === "active" ? "진행 중" : state.status === "awaiting_synthesis" ? "합성 대기" : state.status === "completed" ? "완성" : state.status} (Round ${state.current_round}/${state.max_rounds})\n**참가자:** ${state.speakers.join(", ")}\n**현재 차례:** ${state.current_speaker}\n**축적 응답:** ${state.log.length}건${state.degradation ? `\n\n**환경 상태:**\n${formatDegradationReport(state.degradation)}` : ""}`,
2744
+ text: `📋 **Forum Status** — ${state.id}\n\n**Project:** ${state.project}\n**Topic:** ${state.topic}\n**Status:** ${state.status === "active" ? "active" : state.status === "awaiting_synthesis" ? "awaiting synthesis" : state.status === "completed" ? "completed" : state.status} (Round ${state.current_round}/${state.max_rounds})\n**Participants:** ${state.speakers.join(", ")}\n**Current turn:** ${state.current_speaker}\n**Accumulated responses:** ${state.log.length}${state.degradation ? `\n\n**Environment status:**\n${formatDegradationReport(state.degradation)}` : ""}`,
2731
2745
  }],
2732
2746
  };
2733
2747
  }
@@ -2735,7 +2749,7 @@ server.tool(
2735
2749
 
2736
2750
  server.tool(
2737
2751
  "deliberation_context",
2738
- "현재 프로젝트의 컨텍스트(md 파일들) 로드합니다. CWD + Obsidian 자동 감지.",
2752
+ "Load project context (markdown files). Auto-detects CWD + Obsidian.",
2739
2753
  {},
2740
2754
  async () => {
2741
2755
  const dirs = detectContextDirs();
@@ -2743,7 +2757,7 @@ server.tool(
2743
2757
  return {
2744
2758
  content: [{
2745
2759
  type: "text",
2746
- text: `## 프로젝트 컨텍스트 (${getProjectSlug()})\n\n**소스:** ${dirs.join(", ")}\n\n${context}`,
2760
+ text: `## Project Context (${getProjectSlug()})\n\n**Source:** ${dirs.join(", ")}\n\n${context}`,
2747
2761
  }],
2748
2762
  };
2749
2763
  }
@@ -2751,13 +2765,13 @@ server.tool(
2751
2765
 
2752
2766
  server.tool(
2753
2767
  "deliberation_browser_llm_tabs",
2754
- "현재 브라우저에서 열려 있는 LLM (chatgpt/claude/gemini )을 조회합니다.",
2768
+ "Query LLM tabs currently open in the browser (chatgpt/claude/gemini etc).",
2755
2769
  {},
2756
2770
  async () => {
2757
2771
  const { tabs, note } = await collectBrowserLlmTabs();
2758
2772
  if (tabs.length === 0) {
2759
2773
  const suffix = note ? `\n\n${note}` : "";
2760
- return { content: [{ type: "text", text: `감지된 LLM 탭이 없습니다.${suffix}` }] };
2774
+ return { content: [{ type: "text", text: t(`No LLM tabs detected.${suffix}`, `감지된 LLM 탭이 없습니다.${suffix}`, "en") }] };
2761
2775
  }
2762
2776
 
2763
2777
  const lines = tabs.map((t, i) => `${i + 1}. [${t.browser}] ${t.title}\n ${t.url}`).join("\n");
@@ -2768,17 +2782,17 @@ server.tool(
2768
2782
 
2769
2783
  server.tool(
2770
2784
  "deliberation_route_turn",
2771
- "현재 턴의 speaker에 맞는 transport 자동 결정하고 안내합니다. CLI speaker 자동 응답 경로, 브라우저 speaker는 클립보드 경로로 라우팅합니다.",
2785
+ "Auto-determine and guide the transport for the current turn's speaker. Routes CLI speakers to auto-response and browser speakers to clipboard.",
2772
2786
  {
2773
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
2774
- auto_prepare_clipboard: z.boolean().default(true).describe("브라우저 speaker일 자동으로 클립보드 prepare 실행"),
2775
- prompt: z.string().optional().describe("브라우저 LLM에 추가로 전달할 지시"),
2776
- include_history_entries: z.number().int().min(0).max(12).default(4).describe("프롬프트에 포함할 최근 로그 개수"),
2787
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
2788
+ auto_prepare_clipboard: z.boolean().default(true).describe("Auto-run clipboard prepare for browser speakers"),
2789
+ prompt: z.string().optional().describe("Additional instructions to pass to browser LLM"),
2790
+ include_history_entries: z.number().int().min(0).max(12).default(4).describe("Number of recent log entries to include in prompt"),
2777
2791
  },
2778
2792
  safeToolHandler("deliberation_route_turn", async ({ session_id, auto_prepare_clipboard, prompt, include_history_entries }) => {
2779
2793
  const resolved = resolveSessionId(session_id);
2780
2794
  if (!resolved) {
2781
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
2795
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
2782
2796
  }
2783
2797
  if (resolved === "MULTIPLE") {
2784
2798
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2786,7 +2800,7 @@ server.tool(
2786
2800
 
2787
2801
  const state = loadSession(resolved);
2788
2802
  if (!state || state.status !== "active") {
2789
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
2803
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
2790
2804
  }
2791
2805
 
2792
2806
  const speaker = state.current_speaker;
@@ -2802,9 +2816,9 @@ server.tool(
2802
2816
 
2803
2817
  let guidance;
2804
2818
  if (isSelfSpeaker) {
2805
- guidance = `🟢 **본인 턴입니다.** 당신(${speaker}) 현재 speaker입니다.\n\n` +
2806
- `직접 응답을 작성하여 \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`로 제출하세요.\n\n` +
2807
- `⚠️ **cli_auto_turn 사용 금지**: 자기 자신을 재귀 호출하면 타임아웃이 발생합니다. 반드시 직접 deliberation_respond 사용하세요.`;
2819
+ guidance = `🟢 **It's your turn.** You (${speaker}) are the current speaker.\n\n` +
2820
+ `Write your response and submit via \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`.\n\n` +
2821
+ `⚠️ **Do not use cli_auto_turn**: Recursively calling yourself will cause a timeout. You must use deliberation_respond directly.`;
2808
2822
  } else {
2809
2823
  guidance = formatTransportGuidance(transport, state, speaker);
2810
2824
  }
@@ -2854,25 +2868,25 @@ server.tool(
2854
2868
  channel_used: "browser_auto",
2855
2869
  fallback_reason: null,
2856
2870
  });
2857
- const routeModelInfo = modelSelection.model !== 'default' ? ` | 모델: ${modelSelection.model}` : "";
2858
- extra = `\n\n⚡ 자동 실행 완료! 브라우저 LLM 응답이 자동으로 제출되었습니다. (${waitResult.data.elapsedMs}ms${routeModelInfo})`;
2871
+ const routeModelInfo = modelSelection.model !== 'default' ? ` | model: ${modelSelection.model}` : "";
2872
+ extra = `\n\n⚡ Auto-execution complete! Browser LLM response was automatically submitted. (${waitResult.data.elapsedMs}ms${routeModelInfo})`;
2859
2873
  } else {
2860
2874
  throw new Error(waitResult.error?.message || "no response received");
2861
2875
  }
2862
2876
  } catch (autoErr) {
2863
2877
  const errMsg = autoErr instanceof Error ? autoErr.message : String(autoErr);
2864
- extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). Chrome --remote-debugging-port=9222로 재시작하세요.`;
2878
+ extra = `\n\n⚠️ Auto-execution failed (${errMsg}). Restart Chrome with --remote-debugging-port=9222.`;
2865
2879
  }
2866
2880
  }
2867
2881
 
2868
2882
  const profileInfo = profile
2869
- ? `\n**프로필:** ${profile.type}${profile.url ? ` | ${profile.url}` : ""}${profile.command ? ` | command: ${profile.command}` : ""}`
2883
+ ? `\n**Profile:** ${profile.type}${profile.url ? ` | ${profile.url}` : ""}${profile.command ? ` | command: ${profile.command}` : ""}`
2870
2884
  : "";
2871
2885
 
2872
2886
  return {
2873
2887
  content: [{
2874
2888
  type: "text",
2875
- text: `## 라우팅 — ${state.id}\n\n**현재 speaker:** ${speaker}\n**Transport:** ${transport}${reason ? ` (fallback: ${reason})` : ""}${profileInfo}\n**역할:** ${(state.speaker_roles || {})[speaker] || "free"}\n**Turn ID:** ${turnId || "(없음)"}\n**라운드:** ${state.current_round}/${state.max_rounds}\n**발언 순서:** ${state.ordering_strategy || "cyclic"}\n\n${guidance}${extra}\n\n${PRODUCT_DISCLAIMER}`,
2889
+ text: `## Turn Routing — ${state.id}\n\n**Current speaker:** ${speaker}\n**Transport:** ${transport}${reason ? ` (fallback: ${reason})` : ""}${profileInfo}\n**Role:** ${(state.speaker_roles || {})[speaker] || "free"}\n**Turn ID:** ${turnId || "(none)"}\n**Round:** ${state.current_round}/${state.max_rounds}\n**Ordering:** ${state.ordering_strategy || "cyclic"}\n\n${guidance}${extra}\n\n${PRODUCT_DISCLAIMER}`,
2876
2890
  }],
2877
2891
  };
2878
2892
  })
@@ -2880,16 +2894,16 @@ server.tool(
2880
2894
 
2881
2895
  server.tool(
2882
2896
  "deliberation_browser_auto_turn",
2883
- "브라우저 LLM 자동으로 턴을 전송하고 응답을 수집합니다 (CDP 기반).",
2897
+ "Automatically send a turn to a browser LLM and collect the response (CDP-based).",
2884
2898
  {
2885
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
2886
- provider: z.string().optional().default("chatgpt").describe("LLM 프로바이더 (chatgpt, claude, gemini)"),
2887
- timeout_sec: z.number().optional().default(45).describe("응답 대기 타임아웃 ()"),
2899
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
2900
+ provider: z.string().optional().default("chatgpt").describe("LLM provider (chatgpt, claude, gemini)"),
2901
+ timeout_sec: z.number().optional().default(45).describe("Response wait timeout (seconds)"),
2888
2902
  },
2889
2903
  safeToolHandler("deliberation_browser_auto_turn", async ({ session_id, provider, timeout_sec }) => {
2890
2904
  const resolved = resolveSessionId(session_id);
2891
2905
  if (!resolved) {
2892
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
2906
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
2893
2907
  }
2894
2908
  if (resolved === "MULTIPLE") {
2895
2909
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2897,17 +2911,17 @@ server.tool(
2897
2911
 
2898
2912
  const state = loadSession(resolved);
2899
2913
  if (!state || state.status !== "active") {
2900
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
2914
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
2901
2915
  }
2902
2916
 
2903
2917
  const speaker = state.current_speaker;
2904
2918
  if (speaker === "none") {
2905
- return { content: [{ type: "text", text: "현재 발언 차례인 speaker가 없습니다." }] };
2919
+ return { content: [{ type: "text", text: t("No speaker currently has the turn.", "현재 발언 차례인 speaker가 없습니다.", state?.lang) }] };
2906
2920
  }
2907
2921
 
2908
2922
  const { transport } = resolveTransportForSpeaker(state, speaker);
2909
2923
  if (transport !== "browser_auto" && transport !== "clipboard") {
2910
- return { content: [{ type: "text", text: `speaker "${speaker}"는 브라우저 타입이 아닙니다 (transport: ${transport}). CLI speaker는 deliberation_respond를 사용하세요.` }] };
2924
+ return { content: [{ type: "text", text: t(`Speaker "${speaker}" is not a browser type (transport: ${transport}). CLI speakers should use deliberation_respond.`, `speaker "${speaker}"는 브라우저 타입이 아닙니다 (transport: ${transport}). CLI speaker는 deliberation_respond를 사용하세요.`, state?.lang) }] };
2911
2925
  }
2912
2926
 
2913
2927
  const turnId = state.pending_turn_id || generateTurnId();
@@ -2929,14 +2943,14 @@ server.tool(
2929
2943
  };
2930
2944
  const attachResult = await port.attach(resolved, attachHint);
2931
2945
  if (!attachResult.ok) {
2932
- return { content: [{ type: "text", text: `❌ 브라우저 바인딩 실패: ${attachResult.error.message}\n\n**에러 코드:** ${attachResult.error.code}\n**도메인:** ${attachResult.error.domain}\n\nCDP 디버깅 포트가 활성화된 브라우저가 실행 중인지 확인하세요.\n\`google-chrome --remote-debugging-port=9222\`\n\n${PRODUCT_DISCLAIMER}` }] };
2946
+ return { content: [{ type: "text", text: `❌ Browser tab binding failed: ${attachResult.error.message}\n\n**Error code:** ${attachResult.error.code}\n**Domain:** ${attachResult.error.domain}\n\nEnsure a browser with CDP debugging port is running.\n\`google-chrome --remote-debugging-port=9222\`\n\n${PRODUCT_DISCLAIMER}` }] };
2933
2947
  }
2934
2948
 
2935
2949
  // Step 1.2: Login detection — check if user is logged in to the web LLM
2936
2950
  const loginCheck = await port.checkLogin(resolved);
2937
2951
  if (loginCheck && !loginCheck.loggedIn) {
2938
2952
  await port.detach(resolved);
2939
- return { content: [{ type: "text", text: `⚠️ **${speaker} 로그인 필요** LLM에 로그인되어 있지 않습니다.\n\n**감지된 상태:** ${loginCheck.reason}\n**URL:** ${loginCheck.url || 'N/A'}\n\n이 speaker 건너뜁니다. 브라우저에서 해당 LLM 로그인한 다시 시도하세요.\n\n⛔ **API 호출로 대체하지 마세요.** 로그인되지 않은 speaker는 건너뛰는 것이 올바른 동작입니다.` }] };
2953
+ return { content: [{ type: "text", text: `⚠️ **${speaker} login required**Not logged in to web LLM.\n\n**Detected status:** ${loginCheck.reason}\n**URL:** ${loginCheck.url || 'N/A'}\n\nThis speaker will be skipped. Log in to the LLM in the browser and try again.\n\n⛔ **Do not substitute with API calls.** Skipping unlogged-in speakers is the correct behavior.` }] };
2940
2954
  }
2941
2955
 
2942
2956
  // Step 1.5: Switch model based on context analysis
@@ -2954,7 +2968,7 @@ server.tool(
2954
2968
  return submitDeliberationTurn({
2955
2969
  session_id: resolved,
2956
2970
  speaker,
2957
- content: `[browser_auto 실패 — fallback] ${sendResult.error.message}`,
2971
+ content: `[browser_auto failed — fallback] ${sendResult.error.message}`,
2958
2972
  turn_id: turnId,
2959
2973
  channel_used: "browser_auto_fallback",
2960
2974
  fallback_reason: sendResult.error.code,
@@ -2964,7 +2978,7 @@ server.tool(
2964
2978
  // Step 4: Wait for response
2965
2979
  const waitResult = await port.waitTurnResult(resolved, turnId, timeout_sec);
2966
2980
  if (!waitResult.ok) {
2967
- return { content: [{ type: "text", text: `⏱️ 브라우저 LLM 응답 대기 타임아웃 (${timeout_sec})\n\n**에러:** ${waitResult.error.message}\n\n자동 실행이 타임아웃되었습니다. Chrome --remote-debugging-port=9222로 실행 중인지 확인하세요.\n\n${PRODUCT_DISCLAIMER}` }] };
2981
+ return { content: [{ type: "text", text: `⏱️ Browser LLM response timeout (${timeout_sec}s)\n\n**Error:** ${waitResult.error.message}\n\nAuto-execution timed out. Ensure Chrome is running with --remote-debugging-port=9222.\n\n${PRODUCT_DISCLAIMER}` }] };
2968
2982
  }
2969
2983
 
2970
2984
  // Step 5: Submit the response
@@ -2987,13 +3001,13 @@ server.tool(
2987
3001
  : "";
2988
3002
 
2989
3003
  const modelInfo = modelSelection.model !== 'default'
2990
- ? `\n**모델:** ${modelSelection.model} (${modelSelection.reason})\n**분석:** category=${modelSelection.category}, complexity=${modelSelection.complexity}`
3004
+ ? `\n**Model:** ${modelSelection.model} (${modelSelection.reason})\n**Analysis:** category=${modelSelection.category}, complexity=${modelSelection.complexity}`
2991
3005
  : "";
2992
3006
 
2993
3007
  return {
2994
3008
  content: [{
2995
3009
  type: "text",
2996
- text: `✅ 브라우저 자동 턴 완료!\n\n**Provider:** ${effectiveProvider}\n**Turn ID:** ${turnId}${modelInfo}\n**응답 길이:** ${response.length}자\n**소요 시간:** ${waitResult.data.elapsedMs}ms${degradationInfo}\n\n${result.content[0].text}`,
3010
+ text: `✅ Browser auto-turn complete!\n\n**Provider:** ${effectiveProvider}\n**Turn ID:** ${turnId}${modelInfo}\n**Response length:** ${response.length} chars\n**Elapsed:** ${waitResult.data.elapsedMs}ms${degradationInfo}\n\n${result.content[0].text}`,
2997
3011
  }],
2998
3012
  };
2999
3013
  })
@@ -3001,15 +3015,15 @@ server.tool(
3001
3015
 
3002
3016
  server.tool(
3003
3017
  "deliberation_cli_auto_turn",
3004
- "CLI speaker 자동으로 턴을 전송하고 응답을 수집합니다.",
3018
+ "Automatically send a turn to a CLI speaker and collect the response.",
3005
3019
  {
3006
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
3007
- timeout_sec: z.number().optional().default(120).describe("CLI 응답 대기 타임아웃 ()"),
3020
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3021
+ timeout_sec: z.number().optional().default(120).describe("CLI response wait timeout (seconds)"),
3008
3022
  },
3009
3023
  safeToolHandler("deliberation_cli_auto_turn", async ({ session_id, timeout_sec }) => {
3010
3024
  const resolved = resolveSessionId(session_id);
3011
3025
  if (!resolved) {
3012
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
3026
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
3013
3027
  }
3014
3028
  if (resolved === "MULTIPLE") {
3015
3029
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -3017,27 +3031,27 @@ server.tool(
3017
3031
 
3018
3032
  const state = loadSession(resolved);
3019
3033
  if (!state || state.status !== "active") {
3020
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
3034
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
3021
3035
  }
3022
3036
 
3023
3037
  const speaker = state.current_speaker;
3024
3038
  if (speaker === "none") {
3025
- return { content: [{ type: "text", text: "현재 발언 차례인 speaker가 없습니다." }] };
3039
+ return { content: [{ type: "text", text: t("No speaker currently has the turn.", "현재 발언 차례인 speaker가 없습니다.", state?.lang) }] };
3026
3040
  }
3027
3041
 
3028
3042
  const { transport } = resolveTransportForSpeaker(state, speaker);
3029
3043
  if (transport !== "cli_respond") {
3030
- return { content: [{ type: "text", text: `speaker "${speaker}"는 CLI 타입이 아닙니다 (transport: ${transport}). 브라우저 speaker는 deliberation_browser_auto_turn을 사용하세요.` }] };
3044
+ return { content: [{ type: "text", text: t(`Speaker "${speaker}" is not a CLI type (transport: ${transport}). Browser speakers should use deliberation_browser_auto_turn.`, `speaker "${speaker}"는 CLI 타입이 아닙니다 (transport: ${transport}). 브라우저 speaker는 deliberation_browser_auto_turn을 사용하세요.`, state?.lang) }] };
3031
3045
  }
3032
3046
 
3033
3047
  // Block recursive self-spawn: if the speaker is the same CLI as the caller,
3034
3048
  // spawning it would create infinite recursion and timeout.
3035
3049
  const callerSpeaker = detectCallerSpeaker();
3036
3050
  if (callerSpeaker && speaker === callerSpeaker) {
3037
- return { content: [{ type: "text", text:
3038
- `⚠️ **재귀 호출 차단**: speaker "${speaker}" 현재 오케스트레이터와 동일한 CLI입니다.\n\n` +
3039
- `cli_auto_turn으로 자기 자신을 spawn하면 타임아웃이 발생합니다.\n` +
3040
- `직접 응답을 작성하여 \`deliberation_respond(session_id: "${resolved}", speaker: "${speaker}", content: "...")\`로 제출하세요.`
3051
+ return { content: [{ type: "text", text: t(
3052
+ `⚠️ **Recursive call blocked**: Speaker "${speaker}" is the same CLI as the current orchestrator.\n\nSpawning yourself with cli_auto_turn will cause a timeout.\nWrite your response and submit via \`deliberation_respond(session_id: "${resolved}", speaker: "${speaker}", content: "...")\`.`,
3053
+ `⚠️ **재귀 호출 차단**: speaker "${speaker}"는 현재 오케스트레이터와 동일한 CLI입니다.\n\ncli_auto_turn으로 자기 자신을 spawn하면 타임아웃이 발생합니다.\n직접 응답을 작성하여 \`deliberation_respond(session_id: "${resolved}", speaker: "${speaker}", content: "...")\`로 제출하세요.`,
3054
+ state?.lang)
3041
3055
  }] };
3042
3056
  }
3043
3057
 
@@ -3047,12 +3061,12 @@ server.tool(
3047
3061
 
3048
3062
  const hint = CLI_INVOCATION_HINTS[speaker];
3049
3063
  if (!hint) {
3050
- return { content: [{ type: "text", text: `speaker "${speaker}"에 대한 CLI 호출 정보가 없습니다. CLI_INVOCATION_HINTS에 등록되지 않은 speaker입니다.` }] };
3064
+ return { content: [{ type: "text", text: t(`No CLI invocation info for speaker "${speaker}". This speaker is not registered in CLI_INVOCATION_HINTS.`, `speaker "${speaker}"에 대한 CLI 호출 정보가 없습니다. CLI_INVOCATION_HINTS에 등록되지 않은 speaker입니다.`, state?.lang) }] };
3051
3065
  }
3052
3066
 
3053
3067
  // Check CLI liveness
3054
3068
  if (!checkCliLiveness(hint.cmd)) {
3055
- return { content: [{ type: "text", text: `❌ CLI "${hint.cmd}"가 설치되어 있지 않거나 실행할 수 없습니다.` }] };
3069
+ return { content: [{ type: "text", text: t(`❌ CLI "${hint.cmd}" is not installed or cannot be executed.`, `❌ CLI "${hint.cmd}"가 설치되어 있지 않거나 실행할 수 없습니다.`, state?.lang) }] };
3056
3070
  }
3057
3071
 
3058
3072
  const turnId = state.pending_turn_id || generateTurnId();
@@ -3095,7 +3109,7 @@ server.tool(
3095
3109
 
3096
3110
  const timer = setTimeout(() => {
3097
3111
  child.kill("SIGTERM");
3098
- reject(new Error(`CLI 타임아웃 (${effectiveTimeout})`));
3112
+ reject(new Error(`CLI timeout (${effectiveTimeout}s)`));
3099
3113
  }, effectiveTimeout * 1000);
3100
3114
 
3101
3115
  child.stdout.on("data", (data) => { stdout += data.toString(); });
@@ -3131,7 +3145,7 @@ server.tool(
3131
3145
  appendRuntimeLog("INFO", `CLI_TURN: ${resolved} | speaker: ${speaker} | cli: ${hint.cmd} | elapsed: ${elapsedMs}ms | response_len: ${response.length} | prior_turns: ${speakerPriorTurns} | effective_timeout: ${effectiveTimeout}s`);
3132
3146
 
3133
3147
  if (!response) {
3134
- return { content: [{ type: "text", text: `⚠️ CLI "${speaker}"가 빈 응답을 반환했습니다.` }] };
3148
+ return { content: [{ type: "text", text: t(`⚠️ CLI "${speaker}" returned an empty response.`, `⚠️ CLI "${speaker}"가 빈 응답을 반환했습니다.`, state?.lang) }] };
3135
3149
  }
3136
3150
 
3137
3151
  // Submit the response
@@ -3147,7 +3161,7 @@ server.tool(
3147
3161
  return {
3148
3162
  content: [{
3149
3163
  type: "text",
3150
- text: `✅ CLI 자동 턴 완료!\n\n**Speaker:** ${speaker}\n**CLI:** ${hint.cmd}\n**Turn ID:** ${turnId}\n**응답 길이:** ${response.length}자\n**소요 시간:** ${elapsedMs}ms\n\n${result.content[0].text}`,
3164
+ text: `✅ CLI auto-turn complete!\n\n**Speaker:** ${speaker}\n**CLI:** ${hint.cmd}\n**Turn ID:** ${turnId}\n**Response length:** ${response.length} chars\n**Elapsed:** ${elapsedMs}ms\n\n${result.content[0].text}`,
3151
3165
  }],
3152
3166
  };
3153
3167
 
@@ -3155,7 +3169,7 @@ server.tool(
3155
3169
  return {
3156
3170
  content: [{
3157
3171
  type: "text",
3158
- text: `❌ CLI 자동 실패: ${err.message}\n\n**Speaker:** ${speaker}\n**CLI:** ${hint.cmd}\n\ndeliberation_respond(speaker: "${speaker}", content: "...")로 수동 응답을 제출할 수 있습니다.`,
3172
+ text: `❌ CLI auto-turn failed: ${err.message}\n\n**Speaker:** ${speaker}\n**CLI:** ${hint.cmd}\n\nYou can submit a manual response via deliberation_respond(speaker: "${speaker}", content: "...").`,
3159
3173
  }],
3160
3174
  };
3161
3175
  }
@@ -3164,13 +3178,13 @@ server.tool(
3164
3178
 
3165
3179
  server.tool(
3166
3180
  "deliberation_respond",
3167
- "현재 턴의 응답을 제출합니다.",
3181
+ "Submit a response for the current turn.",
3168
3182
  {
3169
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
3170
- speaker: z.string().trim().min(1).max(64).describe("응답자 이름"),
3171
- content: z.string().optional().describe("응답 내용 (마크다운). content 또는 content_file 하나 필수."),
3172
- content_file: z.string().optional().describe("응답 내용이 담긴 파일 경로. JSON 이스케이프 문제 회피용. 파일 내용이 그대로 content로 사용됩니다."),
3173
- turn_id: z.string().optional().describe(" 검증 ID (deliberation_route_turn에서 받은 )"),
3183
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3184
+ speaker: z.string().trim().min(1).max(64).describe("Responder name"),
3185
+ content: z.string().optional().describe("Response content (markdown). Either content or content_file is required."),
3186
+ content_file: z.string().optional().describe("File path containing response content. For avoiding JSON escape issues. File content is used as-is for content."),
3187
+ turn_id: z.string().optional().describe("Turn verification ID (value received from deliberation_route_turn)"),
3174
3188
  },
3175
3189
  safeToolHandler("deliberation_respond", async ({ session_id, speaker, content, content_file, turn_id }) => {
3176
3190
  // Guard: prevent orchestrator from fabricating responses for CLI/browser speakers
@@ -3187,12 +3201,10 @@ server.tool(
3187
3201
  return {
3188
3202
  content: [{
3189
3203
  type: "text",
3190
- text: `⚠️ **대리 응답 차단**: speaker "${speaker}"는 ${transport} transport입니다.\n\n` +
3191
- `오케스트레이터가 다른 speaker 대신하여 응답을 작성하는 것은 허용되지 않습니다.\n` +
3192
- `대신 다음 도구를 사용하세요:\n` +
3193
- `- CLI speaker → \`deliberation_route_turn\` 또는 \`deliberation_cli_auto_turn\`\n` +
3194
- `- 브라우저 speaker → \`deliberation_route_turn\` 또는 \`deliberation_browser_auto_turn\`\n\n` +
3195
- `이 도구들이 실제 CLI/브라우저를 실행하여 진짜 응답을 수집합니다.`,
3204
+ text: t(
3205
+ `⚠️ **Proxy response blocked**: Speaker "${speaker}" has ${transport} transport.\n\nThe orchestrator is not allowed to write responses on behalf of other speakers.\nUse the following tools instead:\n- CLI speaker → \`deliberation_route_turn\` or \`deliberation_cli_auto_turn\`\n- Browser speaker → \`deliberation_route_turn\` or \`deliberation_browser_auto_turn\`\n\nThese tools run the actual CLI/browser to collect genuine responses.`,
3206
+ `⚠️ **대리 응답 차단**: speaker "${speaker}"는 ${transport} transport입니다.\n\n오케스트레이터가 다른 speaker를 대신하여 응답을 작성하는 것은 허용되지 않습니다.\n대신 다음 도구를 사용하세요:\n- CLI speaker → \`deliberation_route_turn\` 또는 \`deliberation_cli_auto_turn\`\n- 브라우저 speaker → \`deliberation_route_turn\` 또는 \`deliberation_browser_auto_turn\`\n\n이 도구들이 실제 CLI/브라우저를 실행하여 진짜 응답을 수집합니다.`,
3207
+ state?.lang),
3196
3208
  }],
3197
3209
  };
3198
3210
  }
@@ -3206,11 +3218,11 @@ server.tool(
3206
3218
  try {
3207
3219
  finalContent = fs.readFileSync(content_file, "utf-8").trim();
3208
3220
  } catch (e) {
3209
- return { content: [{ type: "text", text: `❌ content_file 읽기 실패: ${e.message}` }] };
3221
+ return { content: [{ type: "text", text: t(`❌ Failed to read content_file: ${e.message}`, `❌ content_file 읽기 실패: ${e.message}`, state?.lang) }] };
3210
3222
  }
3211
3223
  }
3212
3224
  if (!finalContent) {
3213
- return { content: [{ type: "text", text: "❌ content 또는 content_file 중 하나를 제공해야 합니다." }] };
3225
+ return { content: [{ type: "text", text: t("❌ Either content or content_file must be provided.", "❌ content 또는 content_file 중 하나를 제공해야 합니다.", "en") }] };
3214
3226
  }
3215
3227
  return submitDeliberationTurn({ session_id, speaker, content: finalContent, turn_id, channel_used: "cli_respond" });
3216
3228
  })
@@ -3218,14 +3230,14 @@ server.tool(
3218
3230
 
3219
3231
  server.tool(
3220
3232
  "deliberation_history",
3221
- "토론 기록을 반환합니다.",
3233
+ "Return the deliberation history.",
3222
3234
  {
3223
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
3235
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3224
3236
  },
3225
3237
  async ({ session_id }) => {
3226
3238
  const resolved = resolveSessionId(session_id);
3227
3239
  if (!resolved) {
3228
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
3240
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
3229
3241
  }
3230
3242
  if (resolved === "MULTIPLE") {
3231
3243
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -3233,19 +3245,19 @@ server.tool(
3233
3245
 
3234
3246
  const state = loadSession(resolved);
3235
3247
  if (!state) {
3236
- return { content: [{ type: "text", text: `세션 "${resolved}"을 찾을 수 없습니다.` }] };
3248
+ return { content: [{ type: "text", text: t(`Session "${resolved}" not found.`, `세션 "${resolved}"을 찾을 수 없습니다.`, "en") }] };
3237
3249
  }
3238
3250
 
3239
3251
  if (state.log.length === 0) {
3240
3252
  return {
3241
3253
  content: [{
3242
3254
  type: "text",
3243
- text: `**세션:** ${state.id}\n**주제:** ${state.topic}\n\n아직 응답이 없습니다. **${state.current_speaker}**가 먼저 응답하세요.`,
3255
+ text: t(`**Session:** ${state.id}\n**Topic:** ${state.topic}\n\nNo responses yet. **${state.current_speaker}** should respond first.`, `**세션:** ${state.id}\n**주제:** ${state.topic}\n\n아직 응답이 없습니다. **${state.current_speaker}**가 먼저 응답하세요.`, state?.lang),
3244
3256
  }],
3245
3257
  };
3246
3258
  }
3247
3259
 
3248
- let history = `**세션:** ${state.id}\n**주제:** ${state.topic} | **상태:** ${state.status}\n\n`;
3260
+ let history = `**Session:** ${state.id}\n**Topic:** ${state.topic} | **Status:** ${state.status}\n\n`;
3249
3261
  for (const e of state.log) {
3250
3262
  history += `### ${e.speaker} — Round ${e.round}\n\n${e.content}\n\n---\n\n`;
3251
3263
  }
@@ -3255,15 +3267,15 @@ server.tool(
3255
3267
 
3256
3268
  server.tool(
3257
3269
  "deliberation_synthesize",
3258
- "토론을 종료하고 합성 보고서를 제출합니다.",
3270
+ "End the deliberation and submit a synthesis report.",
3259
3271
  {
3260
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
3261
- synthesis: z.string().describe("합성 보고서 (마크다운)"),
3272
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3273
+ synthesis: z.string().describe("Synthesis report (markdown)"),
3262
3274
  },
3263
3275
  safeToolHandler("deliberation_synthesize", async ({ session_id, synthesis }) => {
3264
3276
  const resolved = resolveSessionId(session_id);
3265
3277
  if (!resolved) {
3266
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
3278
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
3267
3279
  }
3268
3280
  if (resolved === "MULTIPLE") {
3269
3281
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -3274,7 +3286,7 @@ server.tool(
3274
3286
  const lockedResult = withSessionLock(resolved, () => {
3275
3287
  const loaded = loadSession(resolved);
3276
3288
  if (!loaded) {
3277
- return { content: [{ type: "text", text: `세션 "${resolved}"을 찾을 수 없습니다.` }] };
3289
+ return { content: [{ type: "text", text: t(`Session "${resolved}" not found.`, `세션 "${resolved}"을 찾을 수 없습니다.`, "en") }] };
3278
3290
  }
3279
3291
 
3280
3292
  loaded.synthesis = synthesis;
@@ -3292,13 +3304,13 @@ server.tool(
3292
3304
 
3293
3305
  appendRuntimeLog("INFO", `SYNTHESIZED: ${resolved} | turns: ${state.log.length} | rounds: ${state.max_rounds}`);
3294
3306
 
3295
- // 토론 종료 즉시 모니터 터미널(물리 Terminal 포함) 강제 종료
3307
+ // Immediately force-close monitor terminal (including physical Terminal) on deliberation end
3296
3308
  closeMonitorTerminal(state.id, getSessionWindowIds(state));
3297
3309
 
3298
3310
  return {
3299
3311
  content: [{
3300
3312
  type: "text",
3301
- text: `✅ [${state.id}] Deliberation 완료! Forum 완성되었습니다.\n\n**프로젝트:** ${state.project}\n**주제:** ${state.topic}\n**라운드:** ${state.max_rounds}\n**응답:** ${state.log.length}건\n\n📁 Forum 최종본: ${archivePath}\n🖥️ 모니터 터미널이 즉시 강제 종료되었습니다.`,
3313
+ text: `✅ [${state.id}] Deliberation complete! Forum finalized.\n\n**Project:** ${state.project}\n**Topic:** ${state.topic}\n**Rounds:** ${state.max_rounds}\n**Responses:** ${state.log.length}\n\n📁 Final forum: ${archivePath}\n🖥️ Monitor terminal force-closed.`,
3302
3314
  }],
3303
3315
  };
3304
3316
  })
@@ -3306,13 +3318,13 @@ server.tool(
3306
3318
 
3307
3319
  server.tool(
3308
3320
  "deliberation_list",
3309
- "과거 deliberation 아카이브 목록을 반환합니다.",
3321
+ "Return the list of past deliberation archives.",
3310
3322
  {},
3311
3323
  async () => {
3312
3324
  ensureDirs();
3313
3325
  const archiveDir = getArchiveDir();
3314
3326
  if (!fs.existsSync(archiveDir)) {
3315
- return { content: [{ type: "text", text: "과거 deliberation이 없습니다." }] };
3327
+ return { content: [{ type: "text", text: t("No past deliberations.", "과거 deliberation이 없습니다.", "en") }] };
3316
3328
  }
3317
3329
 
3318
3330
  const files = fs.readdirSync(archiveDir)
@@ -3320,31 +3332,31 @@ server.tool(
3320
3332
  .sort().reverse();
3321
3333
 
3322
3334
  if (files.length === 0) {
3323
- return { content: [{ type: "text", text: "과거 deliberation이 없습니다." }] };
3335
+ return { content: [{ type: "text", text: t("No past deliberations.", "과거 deliberation이 없습니다.", "en") }] };
3324
3336
  }
3325
3337
 
3326
3338
  const list = files.map((f, i) => `${i + 1}. ${f.replace(".md", "")}`).join("\n");
3327
- return { content: [{ type: "text", text: `## 과거 Deliberation (${getProjectSlug()})\n\n${list}` }] };
3339
+ return { content: [{ type: "text", text: `## Past Deliberations (${getProjectSlug()})\n\n${list}` }] };
3328
3340
  }
3329
3341
  );
3330
3342
 
3331
3343
  server.tool(
3332
3344
  "deliberation_reset",
3333
- "deliberation 초기화합니다. session_id 지정 해당 세션만, 미지정 전체 초기화.",
3345
+ "Reset deliberation. Resets specific session if session_id provided, otherwise resets all.",
3334
3346
  {
3335
- session_id: z.string().optional().describe("초기화할 세션 ID (미지정 전체 초기화)"),
3347
+ session_id: z.string().optional().describe("Session ID to reset (resets all if omitted)"),
3336
3348
  },
3337
3349
  safeToolHandler("deliberation_reset", async ({ session_id }) => {
3338
3350
  ensureDirs();
3339
3351
  const sessionsDir = getSessionsDir();
3340
3352
 
3341
3353
  if (session_id) {
3342
- // 특정 세션만 초기화
3354
+ // Reset specific session only
3343
3355
  let toCloseIds = [];
3344
3356
  const result = withSessionLock(session_id, () => {
3345
3357
  const file = getSessionFile(session_id);
3346
3358
  if (!fs.existsSync(file)) {
3347
- return { content: [{ type: "text", text: `세션 "${session_id}"을 찾을 수 없습니다.` }] };
3359
+ return { content: [{ type: "text", text: t(`Session "${session_id}" not found.`, `세션 "${session_id}"을 찾을 수 없습니다.`, "en") }] };
3348
3360
  }
3349
3361
  const state = loadSession(session_id);
3350
3362
  if (state && state.log.length > 0) {
@@ -3353,7 +3365,7 @@ server.tool(
3353
3365
  if (state) cleanupSyncMarkdown(state);
3354
3366
  toCloseIds = getSessionWindowIds(state);
3355
3367
  fs.unlinkSync(file);
3356
- return { content: [{ type: "text", text: `✅ 세션 "${session_id}" 초기화 완료. 🖥️ 모니터 터미널 닫힘.` }] };
3368
+ return { content: [{ type: "text", text: t(`✅ Session "${session_id}" reset complete. 🖥️ Monitor terminal closed.`, `✅ 세션 "${session_id}" 초기화 완료. 🖥️ 모니터 터미널 닫힘.`, "en") }] };
3357
3369
  });
3358
3370
  if (toCloseIds.length > 0) {
3359
3371
  closeMonitorTerminal(session_id, toCloseIds);
@@ -3361,7 +3373,7 @@ server.tool(
3361
3373
  return result;
3362
3374
  }
3363
3375
 
3364
- // 전체 초기화
3376
+ // Reset all
3365
3377
  const resetResult = withProjectLock(() => {
3366
3378
  if (!fs.existsSync(sessionsDir)) {
3367
3379
  return { files: [], archived: 0, terminalWindowIds: [], noSessions: true };
@@ -3397,7 +3409,7 @@ server.tool(
3397
3409
  });
3398
3410
 
3399
3411
  if (resetResult.noSessions) {
3400
- return { content: [{ type: "text", text: "초기화할 세션이 없습니다." }] };
3412
+ return { content: [{ type: "text", text: t("No sessions to reset.", "초기화할 세션이 없습니다.", "en") }] };
3401
3413
  }
3402
3414
 
3403
3415
  for (const windowId of resetResult.terminalWindowIds) {
@@ -3408,7 +3420,7 @@ server.tool(
3408
3420
  return {
3409
3421
  content: [{
3410
3422
  type: "text",
3411
- text: `✅ 전체 초기화 완료. ${resetResult.files.length} 세션 삭제, ${resetResult.archived} 아카이브됨. 🖥️ 모든 모니터 터미널 닫힘.`,
3423
+ text: `✅ Full reset complete. ${resetResult.files.length} sessions deleted, ${resetResult.archived} archived. 🖥️ All monitor terminals closed.`,
3412
3424
  }],
3413
3425
  };
3414
3426
  })
@@ -3416,19 +3428,21 @@ server.tool(
3416
3428
 
3417
3429
  server.tool(
3418
3430
  "deliberation_cli_config",
3419
- "딜리버레이션 참가자 CLI 설정을 조회하거나 변경합니다. enabled_clis 지정하면 저장합니다.",
3431
+ "Query or update deliberation participant CLI settings. Saves when enabled_clis is provided.",
3420
3432
  {
3421
- enabled_clis: z.array(z.string()).optional().describe("활성화할 CLI 목록 (예: [\"claude\", \"codex\", \"gemini\"]). 미지정 현재 설정 조회"),
3433
+ enabled_clis: z.array(z.string()).optional().describe("CLI list to enable (e.g., [\"claude\", \"codex\", \"gemini\"]). Shows current settings if omitted"),
3422
3434
  require_speaker_selection: z.preprocess(
3423
3435
  (v) => (typeof v === "string" ? v === "true" : v),
3424
3436
  z.boolean().optional()
3425
- ).describe("true: 매번 사용자가 스피커 선택 시작, false: 감지된 스피커 전체 자동 참여"),
3437
+ ).describe("true: user selects speakers before each start, false: all detected speakers auto-join"),
3426
3438
  default_rounds: z.coerce.number().int().min(1).max(10).optional()
3427
- .describe("기본 라운드 (1-10, 기본 3)"),
3439
+ .describe("Default number of rounds (1-10, default 3)"),
3428
3440
  default_ordering: z.enum(["auto", "cyclic", "random", "weighted-random"]).optional()
3429
- .describe("기본 발언 순서 전략: auto(스피커 수에 따라 자동), cyclic, random, weighted-random"),
3441
+ .describe("Default ordering strategy: auto (automatic based on speaker count), cyclic, random, weighted-random"),
3442
+ chrome_profile: z.string().optional()
3443
+ .describe("Chrome profile directory name for CDP (e.g., \"Default\", \"Profile 1\"). Stored for auto-launch."),
3430
3444
  },
3431
- safeToolHandler("deliberation_cli_config", async ({ enabled_clis, require_speaker_selection, default_rounds, default_ordering }) => {
3445
+ safeToolHandler("deliberation_cli_config", async ({ enabled_clis, require_speaker_selection, default_rounds, default_ordering, chrome_profile }) => {
3432
3446
  const config = loadDeliberationConfig();
3433
3447
 
3434
3448
  // Handle setup config updates
@@ -3445,6 +3459,10 @@ server.tool(
3445
3459
  config.default_ordering = default_ordering;
3446
3460
  configChanged = true;
3447
3461
  }
3462
+ if (chrome_profile !== undefined && chrome_profile !== null) {
3463
+ config.chrome_profile = chrome_profile;
3464
+ configChanged = true;
3465
+ }
3448
3466
  if (configChanged) {
3449
3467
  config.setup_complete = true;
3450
3468
  saveDeliberationConfig(config);
@@ -3459,7 +3477,7 @@ server.tool(
3459
3477
  return {
3460
3478
  content: [{
3461
3479
  type: "text",
3462
- text: `## Deliberation CLI 설정\n\n**모드:** ${mode}\n**스피커 선택:** ${config.require_speaker_selection === false ? "자동 (감지된 스피커 전체 참여)" : "수동 (사용자가 직접 선택)"}\n**기본 라운드:** ${config.default_rounds || 3}\n**발언 순서:** ${config.default_ordering || "auto"}\n**설정된 CLI:** ${configured.length > 0 ? configured.join(", ") : "(없음전체 자동 감지)"}\n**현재 감지된 CLI:** ${detected.join(", ") || "(없음)"}\n**지원 CLI 전체:** ${DEFAULT_CLI_CANDIDATES.join(", ")}\n\n변경하려면:\n\`deliberation_cli_config(require_speaker_selection: false, default_rounds: 3, default_ordering: "auto")\`\n\n전체 자동 감지로 되돌리려면:\n\`deliberation_cli_config(enabled_clis: [])\``,
3480
+ text: `## Deliberation CLI Settings\n\n**Mode:** ${mode}\n**Speaker selection:** ${config.require_speaker_selection === false ? "auto (all detected speakers join)" : "manual (user selects)"}\n**Default rounds:** ${config.default_rounds || 3}\n**Ordering:** ${config.default_ordering || "auto"}\n**Chrome profile:** ${config.chrome_profile || "Default"} (env: DELIBERATION_CHROME_PROFILE)\n**Configured CLIs:** ${configured.length > 0 ? configured.join(", ") : "(nonefull auto-detection)"}\n**Currently detected CLIs:** ${detected.join(", ") || "(none)"}\n**All supported CLIs:** ${DEFAULT_CLI_CANDIDATES.join(", ")}\n\nTo change:\n\`deliberation_cli_config(require_speaker_selection: false, default_rounds: 3, default_ordering: "auto")\`\n\nTo set Chrome profile for CDP:\n\`deliberation_cli_config(chrome_profile: "Profile 1")\`\n\nTo revert to full auto-detection:\n\`deliberation_cli_config(enabled_clis: [])\``,
3463
3481
  }],
3464
3482
  };
3465
3483
  }
@@ -3472,7 +3490,7 @@ server.tool(
3472
3490
  return {
3473
3491
  content: [{
3474
3492
  type: "text",
3475
- text: `✅ CLI 설정 초기화 완료. 전체 자동 감지 모드로 전환되었습니다.\n감지 대상: ${DEFAULT_CLI_CANDIDATES.join(", ")}`,
3493
+ text: `✅ CLI settings reset. Switched to full auto-detection mode.\nDetection targets: ${DEFAULT_CLI_CANDIDATES.join(", ")}`,
3476
3494
  }],
3477
3495
  };
3478
3496
  }
@@ -3497,9 +3515,9 @@ server.tool(
3497
3515
  });
3498
3516
  const notInstalled = valid.filter(cli => !installed.includes(cli));
3499
3517
 
3500
- let result = `✅ CLI 설정 저장 완료!\n\n**활성화된 CLI:** ${valid.join(", ")}`;
3501
- if (installed.length > 0) result += `\n**설치 확인됨:** ${installed.join(", ")}`;
3502
- if (notInstalled.length > 0) result += `\n**⚠️ 미설치:** ${notInstalled.join(", ")} (PATH에서 찾을 없음)`;
3518
+ let result = `✅ CLI settings saved!\n\n**Enabled CLIs:** ${valid.join(", ")}`;
3519
+ if (installed.length > 0) result += `\n**Installed:** ${installed.join(", ")}`;
3520
+ if (notInstalled.length > 0) result += `\n**⚠️ Not installed:** ${notInstalled.join(", ")} (not found in PATH)`;
3503
3521
 
3504
3522
  return { content: [{ type: "text", text: result }] };
3505
3523
  })
@@ -3585,15 +3603,15 @@ function synthesizeReviews(context, question, reviews) {
3585
3603
 
3586
3604
  server.tool(
3587
3605
  "deliberation_request_review",
3588
- "코드 리뷰를 요청합니다. 여러 CLI 리뷰어에게 동시에 리뷰를 요청하고 결과를 종합합니다.",
3606
+ "Request a code review. Sends review requests to multiple CLI reviewers simultaneously and synthesizes results.",
3589
3607
  {
3590
- context: z.string().describe("리뷰할 변경사항 설명 (코드, diff, 설계 )"),
3591
- question: z.string().describe("리뷰 질문 (예: 'Is this error handling sufficient?')"),
3592
- reviewers: z.array(z.string().trim().min(1).max(64)).min(1).describe("리뷰어 CLI 목록 (예: [\"claude\", \"codex\"])"),
3593
- mode: z.enum(["sync", "async"]).default("sync").describe("sync: 결과 대기 반환, async: session_id 즉시 반환"),
3594
- deadline_ms: z.number().int().min(5000).max(600000).default(60000).describe("전체 타임아웃 (밀리초, 기본 60초)"),
3595
- min_reviews: z.number().int().min(1).default(1).describe("최소 필요 리뷰 (기본 1)"),
3596
- on_timeout: z.enum(["partial", "fail"]).default("partial").describe("타임아웃 동작: partial=부분 결과 반환, fail=에러"),
3608
+ context: z.string().describe("Description of changes to review (code, diff, design, etc.)"),
3609
+ question: z.string().describe("Review question (e.g., 'Is this error handling sufficient?')"),
3610
+ reviewers: z.array(z.string().trim().min(1).max(64)).min(1).describe("Reviewer CLI list (e.g., [\"claude\", \"codex\"])"),
3611
+ mode: z.enum(["sync", "async"]).default("sync").describe("sync: wait for results then return, async: return session_id immediately"),
3612
+ deadline_ms: z.number().int().min(5000).max(600000).default(60000).describe("Total timeout (milliseconds, default 60s)"),
3613
+ min_reviews: z.number().int().min(1).default(1).describe("Minimum required reviews (default 1)"),
3614
+ on_timeout: z.enum(["partial", "fail"]).default("partial").describe("Timeout behavior: partial=return partial results, fail=error"),
3597
3615
  },
3598
3616
  safeToolHandler("deliberation_request_review", async ({ context, question, reviewers, mode, deadline_ms, min_reviews, on_timeout }) => {
3599
3617
  // Validate reviewers exist in PATH
@@ -3613,7 +3631,7 @@ server.tool(
3613
3631
  return {
3614
3632
  content: [{
3615
3633
  type: "text",
3616
- text: `❌ 유효한 리뷰어가 없습니다. PATH에서 찾을 없는 CLI: ${invalidReviewers.join(", ")}\n\n사용 가능한 CLI를 확인하려면 deliberation_speaker_candidates를 호출하세요.`,
3634
+ text: `❌ No valid reviewers. CLIs not found in PATH: ${invalidReviewers.join(", ")}\n\nCall deliberation_speaker_candidates to check available CLIs.`,
3617
3635
  }],
3618
3636
  };
3619
3637
  }
@@ -3657,12 +3675,12 @@ server.tool(
3657
3675
  // Async mode: return immediately
3658
3676
  if (mode === "async") {
3659
3677
  const warn = invalidReviewers.length > 0
3660
- ? `\n⚠️ PATH에서 찾을 없는 리뷰어 (제외됨): ${invalidReviewers.join(", ")}`
3678
+ ? `\n⚠️ Reviewers not found in PATH (excluded): ${invalidReviewers.join(", ")}`
3661
3679
  : "";
3662
3680
  return {
3663
3681
  content: [{
3664
3682
  type: "text",
3665
- text: `✅ 비동기 리뷰 세션 생성됨\n\n**Session ID:** ${sessionId}\n**리뷰어:** ${validReviewers.join(", ")}\n**모드:** async${warn}\n\n진행 상태는 \`deliberation_status(session_id: "${sessionId}")\`로 확인하세요.`,
3683
+ text: `✅ Async review session created\n\n**Session ID:** ${sessionId}\n**Reviewers:** ${validReviewers.join(", ")}\n**Mode:** async${warn}\n\nCheck progress with \`deliberation_status(session_id: "${sessionId}")\`.`,
3666
3684
  }],
3667
3685
  };
3668
3686
  }
@@ -3734,7 +3752,7 @@ server.tool(
3734
3752
  return {
3735
3753
  content: [{
3736
3754
  type: "text",
3737
- text: `❌ 리뷰 실패: 최소 ${min_reviews} 리뷰 필요, ${completedReviews.length}개만 완료\n\n**Session:** ${sessionId}\n**완료:** ${completedReviews.map(r => r.reviewer).join(", ") || "(없음)"}\n**타임아웃:** ${timedOutReviewers.join(", ") || "(없음)"}\n**실패:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ") || "(없음)"}`,
3755
+ text: `❌ Review failed: minimum ${min_reviews} reviews required, only ${completedReviews.length} completed\n\n**Session:** ${sessionId}\n**Completed:** ${completedReviews.map(r => r.reviewer).join(", ") || "(none)"}\n**Timed out:** ${timedOutReviewers.join(", ") || "(none)"}\n**Failed:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ") || "(none)"}`,
3738
3756
  }],
3739
3757
  };
3740
3758
  }
@@ -3760,13 +3778,13 @@ server.tool(
3760
3778
  const totalMs = Date.now() - globalStart;
3761
3779
  const coverage = `${completedReviews.length}/${validReviewers.length}`;
3762
3780
  const warn = invalidReviewers.length > 0
3763
- ? `\n**제외된 리뷰어 (미설치):** ${invalidReviewers.join(", ")}`
3781
+ ? `\n**Excluded reviewers (not installed):** ${invalidReviewers.join(", ")}`
3764
3782
  : "";
3765
3783
  const timeoutInfo = timedOutReviewers.length > 0
3766
- ? `\n**타임아웃 리뷰어:** ${timedOutReviewers.join(", ")}`
3784
+ ? `\n**Timed out reviewers:** ${timedOutReviewers.join(", ")}`
3767
3785
  : "";
3768
3786
  const failInfo = failedReviewers.length > 0
3769
- ? `\n**실패 리뷰어:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ")}`
3787
+ ? `\n**Failed reviewers:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ")}`
3770
3788
  : "";
3771
3789
 
3772
3790
  const resultPayload = {
@@ -3783,7 +3801,7 @@ server.tool(
3783
3801
  return {
3784
3802
  content: [{
3785
3803
  type: "text",
3786
- text: `## Review 완료\n\n**Session:** ${sessionId}\n**Coverage:** ${coverage}\n**소요 시간:** ${totalMs}ms\n**완료 리뷰어:** ${completedReviews.map(r => r.reviewer).join(", ") || "(없음)"}${timeoutInfo}${failInfo}${warn}\n\n${synthesis}\n\n---\n\n\`\`\`json\n${JSON.stringify(resultPayload, null, 2)}\n\`\`\``,
3804
+ text: `## Review Complete\n\n**Session:** ${sessionId}\n**Coverage:** ${coverage}\n**Elapsed:** ${totalMs}ms\n**Completed reviewers:** ${completedReviews.map(r => r.reviewer).join(", ") || "(none)"}${timeoutInfo}${failInfo}${warn}\n\n${synthesis}\n\n---\n\n\`\`\`json\n${JSON.stringify(resultPayload, null, 2)}\n\`\`\``,
3787
3805
  }],
3788
3806
  };
3789
3807
  })
@@ -3793,22 +3811,22 @@ server.tool(
3793
3811
 
3794
3812
  server.tool(
3795
3813
  "decision_start",
3796
- " 의사결정 세션을 시작합니다. 여러 LLM이 독립적으로 의견을 제시하고 갈등을 가시화합니다.",
3814
+ "Start a new decision session. Multiple LLMs provide independent opinions and conflicts are visualized.",
3797
3815
  {
3798
- problem: z.string().describe("의사결정 문제 (예: 'JWT vs Session 인증 방식 선택')"),
3816
+ problem: z.string().describe("Decision problem (e.g., 'JWT vs Session authentication method')"),
3799
3817
  options: z.preprocess(
3800
3818
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
3801
3819
  z.array(z.string()).optional()
3802
- ).describe("선택지 목록 (예: ['JWT', 'Session', 'OAuth2'])"),
3820
+ ).describe("Options list (e.g., ['JWT', 'Session', 'OAuth2'])"),
3803
3821
  criteria: z.preprocess(
3804
3822
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
3805
3823
  z.array(z.string()).optional()
3806
- ).describe("평가 기준 (미지정 템플릿에서 자동 로드)"),
3807
- template: z.string().optional().describe("Micro-decision 템플릿 ID (lib-compare, arch-decision, pr-priority, naming-convention, tradeoff, risk-approval)"),
3824
+ ).describe("Evaluation criteria (auto-loaded from template if omitted)"),
3825
+ template: z.string().optional().describe("Micro-decision template ID (lib-compare, arch-decision, pr-priority, naming-convention, tradeoff, risk-approval)"),
3808
3826
  speakers: z.preprocess(
3809
3827
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
3810
3828
  z.array(z.string().trim().min(1).max(64)).min(2).optional()
3811
- ).describe("참여 LLM 목록 (최소 2명, 예: ['claude', 'codex', 'gemini'])"),
3829
+ ).describe("Participating LLM list (minimum 2, e.g., ['claude', 'codex', 'gemini'])"),
3812
3830
  },
3813
3831
  safeToolHandler("decision_start", async ({ problem, options, criteria, template, speakers }) => {
3814
3832
  // Auto-discover speakers if not provided
@@ -3819,7 +3837,7 @@ server.tool(
3819
3837
  .map(c => c.speaker)
3820
3838
  .slice(0, 4);
3821
3839
  if (speakers.length < 2) {
3822
- return { content: [{ type: "text", text: "❌ 의사결정에 최소 2명의 speaker가 필요합니다. speakers를 직접 지정하세요." }] };
3840
+ return { content: [{ type: "text", text: t("❌ Decision requires at least 2 speakers. Please specify speakers directly.", "❌ 의사결정에 최소 2명의 speaker가 필요합니다. speakers를 직접 지정하세요.", "en") }] };
3823
3841
  }
3824
3842
  }
3825
3843
 
@@ -3970,7 +3988,7 @@ server.tool(
3970
3988
  return {
3971
3989
  content: [{
3972
3990
  type: "text",
3973
- text: `✅ **Decision Session 시작됨**\n\n**Session:** ${session.id}\n**Problem:** ${problem}\n**Speakers:** ${speakers.join(", ")}\n**Opinions collected:** ${successCount}/${speakers.length}${templateInfo}\n**Stage:** user_probe (사용자 입력 대기)\n**Conflicts:** ${(updatedSession?.conflicts || []).length}개\n\n---\n\n${conflictText}\n\n---\n\n사용자 응답을 \`decision_respond\`로 제출하세요.`,
3991
+ text: `✅ **Decision Session Started**\n\n**Session:** ${session.id}\n**Problem:** ${problem}\n**Speakers:** ${speakers.join(", ")}\n**Opinions collected:** ${successCount}/${speakers.length}${templateInfo}\n**Stage:** user_probe (awaiting user input)\n**Conflicts:** ${(updatedSession?.conflicts || []).length}\n\n---\n\n${conflictText}\n\n---\n\nSubmit user responses via \`decision_respond\`.`,
3974
3992
  }],
3975
3993
  };
3976
3994
  })
@@ -3978,9 +3996,9 @@ server.tool(
3978
3996
 
3979
3997
  server.tool(
3980
3998
  "decision_status",
3981
- "의사결정 세션의 현재 상태를 조회합니다.",
3999
+ "Query the current status of a decision session.",
3982
4000
  {
3983
- session_id: z.string().optional().describe("세션 ID (미지정 활성 decision 세션 자동 선택)"),
4001
+ session_id: z.string().optional().describe("Session ID (auto-selects active decision session if omitted)"),
3984
4002
  },
3985
4003
  safeToolHandler("decision_status", async ({ session_id }) => {
3986
4004
  // Find decision sessions
@@ -3991,13 +4009,13 @@ server.tool(
3991
4009
 
3992
4010
  let resolved = session_id;
3993
4011
  if (!resolved) {
3994
- if (active.length === 0) return { content: [{ type: "text", text: "활성 decision 세션이 없습니다." }] };
4012
+ if (active.length === 0) return { content: [{ type: "text", text: t("No active decision sessions.", "활성 decision 세션이 없습니다.", "en") }] };
3995
4013
  if (active.length === 1) resolved = active[0].id;
3996
- else return { content: [{ type: "text", text: `여러 decision 세션이 진행 중입니다. session_id를 지정하세요:\n${active.map(s => `- ${s.id}`).join("\n")}` }] };
4014
+ else return { content: [{ type: "text", text: t(`Multiple decision sessions are active. Please specify session_id:\n${active.map(s => `- ${s.id}`).join("\n")}`, `여러 decision 세션이 진행 중입니다. session_id를 지정하세요:\n${active.map(s => `- ${s.id}`).join("\n")}`, "en") }] };
3997
4015
  }
3998
4016
 
3999
4017
  const state = loadSession(resolved);
4000
- if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
4018
+ if (!state) return { content: [{ type: "text", text: t(`Session not found: ${resolved}`, `세션을 찾을 수 없습니다: ${resolved}`, "en") }] };
4001
4019
 
4002
4020
  const opinionCount = Object.keys(state.opinions || {}).length;
4003
4021
  const conflictCount = (state.conflicts || []).length;
@@ -4015,13 +4033,13 @@ server.tool(
4015
4033
 
4016
4034
  server.tool(
4017
4035
  "decision_respond",
4018
- "user_probe 단계의 갈등 질문에 대한 사용자 응답을 제출합니다.",
4036
+ "Submit user responses to conflict questions in the user_probe stage.",
4019
4037
  {
4020
- session_id: z.string().optional().describe("세션 ID"),
4038
+ session_id: z.string().optional().describe("Session ID"),
4021
4039
  responses: z.preprocess(
4022
4040
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
4023
4041
  z.array(z.string()).min(1)
4024
- ).describe(" 갈등 질문에 대한 응답 배열 (conflict 순서대로)"),
4042
+ ).describe("Response array for each conflict question (in conflict order)"),
4025
4043
  },
4026
4044
  safeToolHandler("decision_respond", async ({ session_id, responses }) => {
4027
4045
  // Find decision session
@@ -4033,8 +4051,8 @@ server.tool(
4033
4051
  let resolved = session_id;
4034
4052
  if (!resolved) {
4035
4053
  if (active.length === 1) resolved = active[0].id;
4036
- else if (active.length === 0) return { content: [{ type: "text", text: "활성 decision 세션이 없습니다." }] };
4037
- else return { content: [{ type: "text", text: `여러 decision 세션이 진행 중입니다. session_id를 지정하세요.` }] };
4054
+ else if (active.length === 0) return { content: [{ type: "text", text: t("No active decision sessions.", "활성 decision 세션이 없습니다.", "en") }] };
4055
+ else return { content: [{ type: "text", text: t(`Multiple decision sessions are active. Please specify session_id.`, `여러 decision 세션이 진행 중입니다. session_id를 지정하세요.`, "en") }] };
4038
4056
  }
4039
4057
 
4040
4058
  let synthesisText = "";
@@ -4044,7 +4062,7 @@ server.tool(
4044
4062
  const state = loadSession(resolved);
4045
4063
  if (!state) return;
4046
4064
  if (state.stage !== "user_probe") {
4047
- synthesisText = `❌ 현재 단계(${state.stage})에서는 응답을 받을 수 없습니다. user_probe 단계에서만 가능합니다.`;
4065
+ synthesisText = t(`❌ Cannot accept responses at current stage (${state.stage}). Only possible during user_probe stage.`, `❌ 현재 단계(${state.stage})에서는 응답을 받을 수 없습니다. user_probe 단계에서만 가능합니다.`, state?.lang);
4048
4066
  return;
4049
4067
  }
4050
4068
 
@@ -4103,9 +4121,9 @@ server.tool(
4103
4121
 
4104
4122
  server.tool(
4105
4123
  "decision_resume",
4106
- "일시 중지된 decision 세션을 재개합니다 (user_probe 단계에서 갈등 질문을 다시 표시).",
4124
+ "Resume a paused decision session (re-displays conflict questions from the user_probe stage).",
4107
4125
  {
4108
- session_id: z.string().optional().describe("세션 ID"),
4126
+ session_id: z.string().optional().describe("Session ID"),
4109
4127
  },
4110
4128
  safeToolHandler("decision_resume", async ({ session_id }) => {
4111
4129
  const active = listActiveSessions().filter(s => {
@@ -4116,21 +4134,21 @@ server.tool(
4116
4134
  let resolved = session_id;
4117
4135
  if (!resolved) {
4118
4136
  if (active.length === 1) resolved = active[0].id;
4119
- else if (active.length === 0) return { content: [{ type: "text", text: "재개할 decision 세션이 없습니다." }] };
4120
- else return { content: [{ type: "text", text: `여러 세션 중 선택하세요:\n${active.map(s => `- ${s.id}`).join("\n")}` }] };
4137
+ else if (active.length === 0) return { content: [{ type: "text", text: t("No decision sessions to resume.", "재개할 decision 세션이 없습니다.", "en") }] };
4138
+ else return { content: [{ type: "text", text: t(`Select from multiple sessions:\n${active.map(s => `- ${s.id}`).join("\n")}`, `여러 세션 중 선택하세요:\n${active.map(s => `- ${s.id}`).join("\n")}`, "en") }] };
4121
4139
  }
4122
4140
 
4123
4141
  const state = loadSession(resolved);
4124
- if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
4142
+ if (!state) return { content: [{ type: "text", text: t(`Session not found: ${resolved}`, `세션을 찾을 수 없습니다: ${resolved}`, "en") }] };
4125
4143
  if (state.stage !== "user_probe") {
4126
- return { content: [{ type: "text", text: `세션이 user_probe 단계가 아닙니다 (현재: ${state.stage}). 재개할 수 없습니다.` }] };
4144
+ return { content: [{ type: "text", text: t(`Session is not at user_probe stage (current: ${state.stage}). Cannot resume.`, `세션이 user_probe 단계가 아닙니다 (현재: ${state.stage}). 재개할 수 없습니다.`, state?.lang) }] };
4127
4145
  }
4128
4146
 
4129
4147
  const conflictText = generateConflictQuestions(state.conflicts || []);
4130
4148
  return {
4131
4149
  content: [{
4132
4150
  type: "text",
4133
- text: `📋 **Decision Session 재개**\n\n**Session:** ${state.id}\n**Problem:** ${state.problem}\n**Stage:** user_probe\n\n---\n\n${conflictText}\n\n---\n\n사용자 응답을 \`decision_respond\`로 제출하세요.`,
4151
+ text: `📋 **Decision Session Resumed**\n\n**Session:** ${state.id}\n**Problem:** ${state.problem}\n**Stage:** user_probe\n\n---\n\n${conflictText}\n\n---\n\nSubmit user responses via \`decision_respond\`.`,
4134
4152
  }],
4135
4153
  };
4136
4154
  })
@@ -4138,14 +4156,14 @@ server.tool(
4138
4156
 
4139
4157
  server.tool(
4140
4158
  "decision_history",
4141
- "과거 의사결정 기록을 조회합니다.",
4159
+ "Query past decision history.",
4142
4160
  {
4143
- session_id: z.string().optional().describe("특정 세션 ID (미지정 전체 목록)"),
4161
+ session_id: z.string().optional().describe("Specific session ID (shows full list if omitted)"),
4144
4162
  },
4145
4163
  safeToolHandler("decision_history", async ({ session_id }) => {
4146
4164
  if (session_id) {
4147
4165
  const state = loadSession(session_id);
4148
- if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${session_id}` }] };
4166
+ if (!state) return { content: [{ type: "text", text: t(`Session not found: ${session_id}`, `세션을 찾을 수 없습니다: ${session_id}`, "en") }] };
4149
4167
 
4150
4168
  const opinionSummary = Object.entries(state.opinions || {})
4151
4169
  .map(([speaker, op]) => `- **${speaker}**: ${op.summary || "(none)"} (confidence: ${Math.round((op.confidence || 0.5) * 100)}%)`)
@@ -4191,12 +4209,12 @@ server.tool(
4191
4209
 
4192
4210
  server.tool(
4193
4211
  "decision_templates",
4194
- "사용 가능한 Micro-Decision 템플릿 목록을 표시합니다.",
4212
+ "Display available Micro-Decision templates.",
4195
4213
  {},
4196
4214
  safeToolHandler("decision_templates", async () => {
4197
4215
  const templates = loadTemplates();
4198
4216
  if (templates.length === 0) {
4199
- return { content: [{ type: "text", text: "사용 가능한 템플릿이 없습니다." }] };
4217
+ return { content: [{ type: "text", text: t("No available templates.", "사용 가능한 템플릿이 없습니다.", "en") }] };
4200
4218
  }
4201
4219
 
4202
4220
  const list = templates.map(t => {
@@ -4207,7 +4225,7 @@ server.tool(
4207
4225
  return {
4208
4226
  content: [{
4209
4227
  type: "text",
4210
- text: `📋 **Decision Templates**\n\n${list}\n\n---\n\n\`decision_start(problem: "...", template: "lib-compare")\`로 사용하세요.`,
4228
+ text: `📋 **Decision Templates**\n\n${list}\n\n---\n\nUse with \`decision_start(problem: "...", template: "lib-compare")\`.`,
4211
4229
  }],
4212
4230
  };
4213
4231
  })