@dmsdc-ai/aigentry-deliberation 0.0.27 → 0.0.28

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,7 +940,7 @@ 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
 
@@ -986,7 +987,7 @@ async function ensureCdpAvailable() {
986
987
  } catch {
987
988
  return {
988
989
  available: false,
989
- reason: `Chrome 자동 실행에 실패했습니다. Chrome 수동으로 --remote-debugging-port=9222 --user-data-dir=~/.chrome-cdp 옵션과 함께 실행해주세요.`,
990
+ reason: `Failed to auto-launch Chrome. Manually run Chrome with --remote-debugging-port=9222 --user-data-dir=~/.chrome-cdp.`,
990
991
  };
991
992
  }
992
993
 
@@ -1005,20 +1006,20 @@ async function ensureCdpAvailable() {
1005
1006
 
1006
1007
  return {
1007
1008
  available: false,
1008
- reason: "Chrome 실행했지만 CDP에 연결할 없습니다. Chrome을 완전히 종료한 다시 시도해주세요. (이미 실행 중인 Chrome CDP 없이 시작된 경우 재시작 필요)",
1009
+ reason: "Chrome launched but cannot connect to CDP. Fully close Chrome and try again. (Restart required if Chrome was started without CDP)",
1009
1010
  };
1010
1011
  }
1011
1012
 
1012
1013
  // Unreachable (all platforms handled above), but keep as safety net
1013
1014
  return {
1014
1015
  available: false,
1015
- reason: "Chrome CDP 활성화할 없습니다. Chrome을 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
1016
+ reason: "Cannot activate Chrome CDP. Run Chrome with --remote-debugging-port=9222.",
1016
1017
  };
1017
1018
  }
1018
1019
 
1019
1020
  function collectBrowserLlmTabsViaAppleScript() {
1020
1021
  if (process.platform !== "darwin") {
1021
- return { tabs: [], note: "AppleScript 스캔은 macOS에서만 지원됩니다." };
1022
+ return { tabs: [], note: "AppleScript tab scanning is only supported on macOS." };
1022
1023
  }
1023
1024
 
1024
1025
  const escapedDomains = DEFAULT_LLM_DOMAINS.map(d => d.replace(/"/g, '\\"'));
@@ -1104,14 +1105,14 @@ return outText`;
1104
1105
  return {
1105
1106
  tabs,
1106
1107
  note: errors.length > 0
1107
- ? `일부 브라우저 접근 실패: ${errors.map(e => `${e.browser} (${e.url})`).join(", ")}`
1108
+ ? `Some browser access failed: ${errors.map(e => `${e.browser} (${e.url})`).join(", ")}`
1108
1109
  : null,
1109
1110
  };
1110
1111
  } catch (error) {
1111
1112
  const reason = error instanceof Error ? error.message : "unknown error";
1112
1113
  return {
1113
1114
  tabs: [],
1114
- note: `브라우저 스캔 실패: ${reason}. macOS 자동화 권한(터미널 -> 브라우저 제어)을 확인하세요.`,
1115
+ note: `Browser tab scan failed: ${reason}. Check macOS automation permissions (Terminal -> Browser control).`,
1115
1116
  };
1116
1117
  }
1117
1118
  }
@@ -1128,7 +1129,7 @@ async function collectBrowserLlmTabs() {
1128
1129
  if (mode === "off") {
1129
1130
  return {
1130
1131
  tabs: dedupeBrowserTabs(tabs),
1131
- note: notes.length > 0 ? notes.join(" | ") : "브라우저 자동 스캔이 비활성화되었습니다.",
1132
+ note: notes.length > 0 ? notes.join(" | ") : "Browser tab auto-scanning is disabled.",
1132
1133
  };
1133
1134
  }
1134
1135
 
@@ -1138,7 +1139,7 @@ async function collectBrowserLlmTabs() {
1138
1139
  tabs.push(...mac.tabs);
1139
1140
  if (mac.note) notes.push(mac.note);
1140
1141
  } else if (mode === "applescript" && process.platform !== "darwin") {
1141
- notes.push("AppleScript 스캔은 macOS 전용입니다. CDP 스캔으로 전환하세요.");
1142
+ notes.push("AppleScript scanning is macOS only. Switch to CDP scanning.");
1142
1143
  }
1143
1144
 
1144
1145
  const shouldUseCdp = mode === "auto" || mode === "cdp";
@@ -1211,7 +1212,7 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
1211
1212
  // Ensure CDP is available before probing browser tabs
1212
1213
  const cdpStatus = await ensureCdpAvailable();
1213
1214
  if (cdpStatus.launched) {
1214
- browserNote = "Chrome CDP 자동 실행됨 (--remote-debugging-port=9222)";
1215
+ browserNote = "Chrome CDP auto-launched (--remote-debugging-port=9222)";
1215
1216
  }
1216
1217
 
1217
1218
  const { tabs, note } = await collectBrowserLlmTabs();
@@ -1345,28 +1346,28 @@ function formatSpeakerCandidatesReport({ candidates, browserNote }) {
1345
1346
  let out = "## Selectable Speakers\n\n";
1346
1347
  out += "### CLI\n";
1347
1348
  if (cli.length === 0) {
1348
- out += "- (감지된 로컬 CLI 없음)\n\n";
1349
+ out += "- (No local CLI detected)\n\n";
1349
1350
  } else {
1350
1351
  out += `${cli.map(c => {
1351
- const status = c.live === false ? " ❌ 실행 불가" : c.live === true ? " ✅ 실행 가능" : "";
1352
+ const status = c.live === false ? " ❌ not executable" : c.live === true ? " ✅ executable" : "";
1352
1353
  return `- \`${c.speaker}\` (command: ${c.command})${status}`;
1353
1354
  }).join("\n")}\n\n`;
1354
1355
  }
1355
1356
 
1356
- out += "### Browser LLM (감지됨)\n";
1357
+ out += "### Browser LLM (detected)\n";
1357
1358
  if (detected.length === 0) {
1358
- out += "- (브라우저에서 감지된 LLM 없음)\n";
1359
+ out += "- (No LLM tabs detected in browser)\n";
1359
1360
  } else {
1360
1361
  out += `${detected.map(c => {
1361
- const icon = c.cdp_available ? "⚡자동" : "📋클립보드";
1362
+ const icon = c.cdp_available ? "⚡auto" : "📋clipboard";
1362
1363
  const extTag = String(c.url || "").startsWith("chrome-extension://") ? " [Extension]" : "";
1363
1364
  return `- \`${c.speaker}\` [${icon}]${extTag} [${c.browser}] ${c.title}\n ${c.url}`;
1364
1365
  }).join("\n")}\n`;
1365
1366
  }
1366
1367
 
1367
- out += "\n### Web LLM (자동 등록)\n";
1368
+ out += "\n### Web LLM (auto-registered)\n";
1368
1369
  out += `${autoReg.map(c => {
1369
- const icon = c.cdp_available ? "⚡자동" : "📋클립보드";
1370
+ const icon = c.cdp_available ? "⚡auto" : "📋clipboard";
1370
1371
  return `- \`${c.speaker}\` [${icon}] — ${c.title} (${c.url})`;
1371
1372
  }).join("\n")}\n`;
1372
1373
 
@@ -1477,11 +1478,11 @@ function resolveTransportForSpeaker(state, speaker) {
1477
1478
 
1478
1479
  // CLI-specific invocation flags for non-interactive execution
1479
1480
  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' },
1481
+ claude: { cmd: "claude", flags: '-p --output-format text', example: 'CLAUDECODE= claude -p --output-format text "prompt"', envPrefix: 'CLAUDECODE=', modelFlag: '--model', provider: 'claude' },
1482
+ 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' },
1483
+ gemini: { cmd: "gemini", flags: '', example: 'gemini "prompt"', modelFlag: '--model', provider: 'gemini' },
1484
+ aider: { cmd: "aider", flags: '--message', example: 'aider --message "prompt"', modelFlag: '--model', provider: 'chatgpt' },
1485
+ cursor: { cmd: "cursor", flags: '', example: 'cursor "prompt"', modelFlag: null, provider: 'chatgpt' },
1485
1486
  };
1486
1487
 
1487
1488
  function formatTransportGuidance(transport, state, speaker) {
@@ -1493,23 +1494,23 @@ function formatTransportGuidance(transport, state, speaker) {
1493
1494
  let modelGuide = "";
1494
1495
  if (hint) {
1495
1496
  const prefix = hint.envPrefix || '';
1496
- invocationGuide = `\n\n**CLI 호출 방법:** \`${hint.example}\`\n(플래그: \`${prefix}${hint.cmd} ${hint.flags}\`)`;
1497
+ invocationGuide = `\n\n**CLI invocation:** \`${hint.example}\`\n(flags: \`${prefix}${hint.cmd} ${hint.flags}\`)`;
1497
1498
  if (hint.modelFlag && hint.provider) {
1498
1499
  const cliModel = getModelSelectionForTurn(state, speaker, hint.provider);
1499
1500
  if (cliModel.model !== 'default') {
1500
- modelGuide = `\n**추천 모델:** ${cliModel.model} (${cliModel.reason})\n**모델 플래그:** \`${hint.modelFlag} ${cliModel.model}\``;
1501
+ modelGuide = `\n**Recommended model:** ${cliModel.model} (${cliModel.reason})\n**Model flag:** \`${hint.modelFlag} ${cliModel.model}\``;
1501
1502
  }
1502
1503
  }
1503
1504
  }
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 도구만 사용하세요.`;
1505
+ 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
1506
  }
1506
1507
  case "clipboard":
1507
- return `브라우저 LLM speaker입니다. CDP 자동 연결 시도 중... Chrome이 이미 CDP 없이 실행 중이면 재시작이 필요할 수 있습니다.\n\n⛔ **API 호출 금지**: speaker 브라우저로만 응답합니다. REST API, HTTP 요청으로 LLM을 호출하지 마세요.`;
1508
+ 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
1509
  case "browser_auto":
1509
- return `자동 브라우저 speaker입니다. \`deliberation_browser_auto_turn(session_id: "${sid}")\`으로 자동 진행됩니다. CDP를 통해 브라우저 LLM 직접 입력하고 응답을 읽습니다.\n\n⛔ **API 호출 금지**: CDP 자동화로만 진행합니다. REST API, HTTP 요청 사용 금지.`;
1510
+ 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
1511
  case "manual":
1511
1512
  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 참여가 거부됩니다.`;
1513
+ 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
1514
  }
1514
1515
  }
1515
1516
 
@@ -1641,7 +1642,7 @@ function readContextFromDirs(dirs, maxChars = 15000) {
1641
1642
  }
1642
1643
  }
1643
1644
  }
1644
- return context || "(컨텍스트 파일을 찾을 수 없습니다)";
1645
+ return context || "(No context files found)";
1645
1646
  }
1646
1647
 
1647
1648
  // ── State helpers ──────────────────────────────────────────────
@@ -1681,15 +1682,15 @@ function listActiveSessions() {
1681
1682
  }
1682
1683
 
1683
1684
  function resolveSessionId(sessionId) {
1684
- // session_id 주어지면 그대로 사용
1685
+ // Use session_id directly if provided
1685
1686
  if (sessionId) return sessionId;
1686
1687
 
1687
- // 없으면 활성 세션이 1개일 자동 선택
1688
+ // Auto-select when only one active session
1688
1689
  const active = listActiveSessions();
1689
1690
  if (active.length === 0) return null;
1690
1691
  if (active.length === 1) return active[0].id;
1691
1692
 
1692
- // 여러 개면 null (목록 표시 필요)
1693
+ // null if multiple (need to show list)
1693
1694
  return "MULTIPLE";
1694
1695
  }
1695
1696
 
@@ -1775,7 +1776,7 @@ const MONITOR_SCRIPT = path.join(INSTALL_DIR, "session-monitor.sh");
1775
1776
  const MONITOR_SCRIPT_WIN = path.join(INSTALL_DIR, "session-monitor-win.js");
1776
1777
 
1777
1778
  function tmuxWindowName(sessionId) {
1778
- // tmux 윈도우 이름은 짧게 (마지막 부분 제거하고 20)
1779
+ // Keep tmux window name short (remove last part, 20 chars)
1779
1780
  return sessionId.replace(/[^a-zA-Z0-9가-힣-]/g, "").slice(0, 25);
1780
1781
  }
1781
1782
 
@@ -2243,13 +2244,13 @@ function closeAllMonitorTerminals() {
2243
2244
  function multipleSessionsError() {
2244
2245
  const active = listActiveSessions();
2245
2246
  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}`;
2247
+ return t(`Multiple active sessions found. Please specify session_id:\n\n${list}`, `여러 활성 세션이 있습니다. session_id를 지정하세요:\n\n${list}`, "en");
2247
2248
  }
2248
2249
 
2249
2250
  function formatRecentLogForPrompt(state, maxEntries = 4) {
2250
2251
  const entries = Array.isArray(state.log) ? state.log.slice(-Math.max(0, maxEntries)) : [];
2251
2252
  if (entries.length === 0) {
2252
- return "(아직 이전 응답 없음)";
2253
+ return "(No previous responses yet)";
2253
2254
  }
2254
2255
  return entries.map(e => {
2255
2256
  const content = String(e.content || "").trim();
@@ -2259,7 +2260,7 @@ function formatRecentLogForPrompt(state, maxEntries = 4) {
2259
2260
 
2260
2261
  function buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries = 4) {
2261
2262
  const recent = formatRecentLogForPrompt(state, includeHistoryEntries);
2262
- const extraPrompt = prompt ? `\n[추가 지시]\n${prompt}\n` : "";
2263
+ const extraPrompt = prompt ? `\n[Additional instructions]\n${prompt}\n` : "";
2263
2264
 
2264
2265
  // Role prompt injection
2265
2266
  const speakerRole = (state.speaker_roles || {})[speaker] || "free";
@@ -2281,9 +2282,9 @@ ${recent}
2281
2282
  [/recent_log]${extraPrompt}
2282
2283
 
2283
2284
  [response_rule]
2284
- - 토론 맥락을 반영해 ${speaker} 이번 응답만 작성
2285
- - 마크다운 본문만 출력 (불필요한 머리말/꼬리말 금지)${speakerRole !== "free" ? `\n- 배정된 역할(${speakerRole}) 관점에서 분석하고 응답` : ""}
2286
- - 응답 마지막에 반드시 [AGREE], [DISAGREE], 또는 [CONDITIONAL: 사유] 하나를 포함
2285
+ - Write only ${speaker}'s response for this turn reflecting the discussion context above
2286
+ - Output markdown body only (no unnecessary headers/footers)${speakerRole !== "free" ? `\n- Analyze and respond from the perspective of assigned role (${speakerRole})` : ""}
2287
+ - Must include one of [AGREE], [DISAGREE], or [CONDITIONAL: reason] at the end of response
2287
2288
  [/response_rule]
2288
2289
  [/deliberation_turn_request]
2289
2290
  `;
@@ -2292,7 +2293,7 @@ ${recent}
2292
2293
  function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel_used, fallback_reason }) {
2293
2294
  const resolved = resolveSessionId(session_id);
2294
2295
  if (!resolved) {
2295
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
2296
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
2296
2297
  }
2297
2298
  if (resolved === "MULTIPLE") {
2298
2299
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2301,12 +2302,12 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2301
2302
  return withSessionLock(resolved, () => {
2302
2303
  const state = loadSession(resolved);
2303
2304
  if (!state || state.status !== "active") {
2304
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
2305
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
2305
2306
  }
2306
2307
 
2307
2308
  const normalizedSpeaker = normalizeSpeaker(speaker);
2308
2309
  if (!normalizedSpeaker) {
2309
- return { content: [{ type: "text", text: "speaker 값이 비어 있습니다. 응답자 이름을 지정하세요." }] };
2310
+ return { content: [{ type: "text", text: t("Speaker value is empty. Please specify a speaker name.", "speaker 값이 비어 있습니다. 응답자 이름을 지정하세요.", "en") }] };
2310
2311
  }
2311
2312
 
2312
2313
  state.speakers = buildSpeakerOrder(state.speakers, state.current_speaker, "end");
@@ -2321,17 +2322,17 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2321
2322
  return {
2322
2323
  content: [{
2323
2324
  type: "text",
2324
- text: `[${state.id}] 지금은 **${state.current_speaker}** 차례입니다. ${normalizedSpeaker}는 대기하세요.`,
2325
+ text: t(`[${state.id}] It is currently **${state.current_speaker}**'s turn. ${normalizedSpeaker} please wait.`, `[${state.id}] 지금은 **${state.current_speaker}** 차례입니다. ${normalizedSpeaker}는 대기하세요.`, state?.lang),
2325
2326
  }],
2326
2327
  };
2327
2328
  }
2328
2329
 
2329
- // turn_id 검증 (선택적제공 반드시 일치해야 함)
2330
+ // turn_id validation (optionalmust match if provided)
2330
2331
  if (turn_id && state.pending_turn_id && turn_id !== state.pending_turn_id) {
2331
2332
  return {
2332
2333
  content: [{
2333
2334
  type: "text",
2334
- text: `[${state.id}] turn_id 불일치. 예상: "${state.pending_turn_id}", 수신: "${turn_id}". 오래된 요청이거나 중복 제출일 수 있습니다.`,
2335
+ 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
2336
  }],
2336
2337
  };
2337
2338
  }
@@ -2372,7 +2373,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2372
2373
  return {
2373
2374
  content: [{
2374
2375
  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}")로 합성 보고서를 작성하세요.`,
2376
+ 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
2377
  }],
2377
2378
  };
2378
2379
  }
@@ -2387,7 +2388,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2387
2388
  return {
2388
2389
  content: [{
2389
2390
  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})`,
2391
+ 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
2392
  }],
2392
2393
  };
2393
2394
  });
@@ -2426,11 +2427,11 @@ const server = new McpServer({
2426
2427
 
2427
2428
  server.tool(
2428
2429
  "deliberation_start",
2429
- " deliberation을 시작합니다. 여러 토론을 동시에 진행할 있습니다.",
2430
+ "Start a new deliberation. Multiple deliberations can run simultaneously.",
2430
2431
  {
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의 항목)"),
2432
+ topic: z.string().describe("Discussion topic"),
2433
+ rounds: z.coerce.number().optional().describe("Number of rounds (defaults to config setting, default 3)"),
2434
+ first_speaker: z.string().trim().min(1).max(64).optional().describe("First speaker name (defaults to first item in speakers)"),
2434
2435
  speakers: z.preprocess(
2435
2436
  (v) => {
2436
2437
  const parsed = typeof v === "string" ? JSON.parse(v) : v;
@@ -2439,31 +2440,31 @@ server.tool(
2439
2440
  return parsed.map(item => (typeof item === "object" && item !== null && item.name) ? item.name : item);
2440
2441
  },
2441
2442
  z.array(z.string().trim().min(1).max(64)).min(1).optional()
2442
- ).describe("참가자 이름 목록. 문자열 배열 또는 {name, role, instructions} 객체 배열 모두 지원"),
2443
+ ).describe("Participant name list. Supports both string arrays and {name, role, instructions} object arrays"),
2443
2444
  speaker_instructions: z.preprocess(
2444
2445
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
2445
2446
  z.record(z.string(), z.string()).optional()
2446
- ).describe("speaker 추가 지시사항 (예: {\"claude\": \"비판적으로 검토\"})"),
2447
+ ).describe("Per-speaker additional instructions (e.g., {\"claude\": \"review critically\"})"),
2447
2448
  require_manual_speakers: z.preprocess(
2448
2449
  (v) => (typeof v === "string" ? v === "true" : v),
2449
2450
  z.boolean().optional()
2450
- ).describe("true speakers 반드시 직접 지정해야 시작 (미지정 config 설정 따름)"),
2451
+ ).describe("If true, speakers must be explicitly specified to start (defaults to config setting)"),
2451
2452
  auto_discover_speakers: z.preprocess(
2452
2453
  (v) => (typeof v === "string" ? v === "true" : v),
2453
2454
  z.boolean().optional()
2454
- ).describe("speakers 생략 자동 탐색 여부 (미지정 config 설정 따름)"),
2455
+ ).describe("Whether to auto-discover speakers when omitted (defaults to config setting)"),
2455
2456
  participant_types: z.preprocess(
2456
2457
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
2457
2458
  z.record(z.string(), z.enum(["cli", "browser", "browser_auto", "manual"])).optional()
2458
- ).describe("speaker 타입 오버라이드 (예: {\"chatgpt\": \"browser_auto\"})"),
2459
+ ).describe("Per-speaker type override (e.g., {\"chatgpt\": \"browser_auto\"})"),
2459
2460
  ordering_strategy: z.enum(["auto", "cyclic", "random", "weighted-random"]).optional()
2460
- .describe("발언 순서 전략: auto(스피커 수에 따라 자동), cyclic(순서대로), random(매턴 무작위), weighted-random( 말한 사람 우선)"),
2461
+ .describe("Ordering strategy: auto (automatic based on speaker count), cyclic (sequential), random (random each turn), weighted-random (less spoken speakers first)"),
2461
2462
  speaker_roles: z.preprocess(
2462
2463
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
2463
2464
  z.record(z.string(), z.enum(["critic", "implementer", "mediator", "researcher", "free"])).optional()
2464
- ).describe("speaker 역할 배정 (예: {\"claude\": \"critic\", \"codex\": \"implementer\"})"),
2465
+ ).describe("Per-speaker role assignment (e.g., {\"claude\": \"critic\", \"codex\": \"implementer\"})"),
2465
2466
  role_preset: z.enum(["balanced", "debate", "research", "brainstorm", "review", "consensus"]).optional()
2466
- .describe("역할 프리셋 (balanced/debate/research/brainstorm/review/consensus). speaker_roles 명시되면 무시됨"),
2467
+ .describe("Role preset (balanced/debate/research/brainstorm/review/consensus). Ignored if speaker_roles is specified"),
2467
2468
  },
2468
2469
  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
2470
  // ── First-time onboarding guard ──
@@ -2474,7 +2475,7 @@ server.tool(
2474
2475
  return {
2475
2476
  content: [{
2476
2477
  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"\` — 발언한 사람 우선`,
2478
+ 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
2479
  }],
2479
2480
  };
2480
2481
  }
@@ -2501,15 +2502,15 @@ server.tool(
2501
2502
  if (!hasManualSpeakers && effectiveRequireManual) {
2502
2503
  const candidateText = formatSpeakerCandidatesReport(candidateSnapshot);
2503
2504
  const llmSuggested = Array.isArray(speakers) && speakers.length > 0
2504
- ? `\n\n💡 **LLM 제안한 스피커:** ${speakers.join(", ")}\n위 제안을 사용하려면 \`require_manual_speakers: true\`와 함께 speakers를 다시 전달하세요.`
2505
+ ? `\n\n💡 **LLM suggested speakers:** ${speakers.join(", ")}\nTo use this suggestion, pass speakers again with \`require_manual_speakers: true\`.`
2505
2506
  : "";
2506
2507
  const configNote = configRequiresSelection
2507
- ? "\n\n⚙️ `require_speaker_selection: true` 설정에 의해 사용자가 직접 스피커를 선택해야 합니다."
2508
+ ? "\n\n⚙️ `require_speaker_selection: true` setting requires you to manually select speakers."
2508
2509
  : "";
2509
2510
  return {
2510
2511
  content: [{
2511
2512
  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 호출해 현재 선택 가능한 스피커를 확인하세요.`,
2513
+ 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
2514
  }],
2514
2515
  };
2515
2516
  }
@@ -2553,7 +2554,7 @@ server.tool(
2553
2554
  return {
2554
2555
  content: [{
2555
2556
  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"])`,
2557
+ 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
2558
  }],
2558
2559
  };
2559
2560
  }
@@ -2570,12 +2571,12 @@ server.tool(
2570
2571
  // cli_auto_turn will handle runtime errors per-turn.
2571
2572
  let detectWarningLiveness = "";
2572
2573
  if (nonLiveCli.length > 0) {
2573
- detectWarningLiveness = `\n\n⚠️ 일부 CLI가 현재 실행 불가 상태이지만 사용자 선택을 존중하여 진행합니다:\n${nonLiveCli.map(s => ` - \`${s}\` ❌`).join("\n")}\n턴 진행 CLI 실행을 재시도합니다. 실패 해당 턴에서 오류가 보고됩니다.`;
2574
+ 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
2575
  }
2575
2576
 
2576
2577
  const participantMode = hasManualSpeakers
2577
- ? "수동 지정"
2578
- : (autoDiscoveredSpeakers.length > 0 ? "자동 탐색(PATH)" : "기본값");
2578
+ ? "manually specified"
2579
+ : (autoDiscoveredSpeakers.length > 0 ? "auto-discovered (PATH)" : "default");
2579
2580
 
2580
2581
  const degradationLevels = await detectDegradationLevels();
2581
2582
 
@@ -2583,6 +2584,7 @@ server.tool(
2583
2584
  id: sessionId,
2584
2585
  project: getProjectSlug(),
2585
2586
  topic,
2587
+ lang: detectLang(topic),
2586
2588
  status: "active",
2587
2589
  max_rounds: rounds,
2588
2590
  current_round: 1,
@@ -2610,7 +2612,7 @@ server.tool(
2610
2612
  return {
2611
2613
  content: [{
2612
2614
  type: "text",
2613
- text: `❌ 브라우저 LLM speaker가 포함되어 있지만 CDP에 연결할 없습니다.\n\n${cdpReady.reason}\n\nCDP 연결 다시 deliberation_start를 호출하세요.`,
2615
+ text: `❌ Browser LLM speakers included but cannot connect to CDP.\n\n${cdpReady.reason}\n\nCall deliberation_start again after establishing CDP connection.`,
2614
2616
  }],
2615
2617
  };
2616
2618
  }
@@ -2641,20 +2643,20 @@ server.tool(
2641
2643
  const isWin = process.platform === "win32";
2642
2644
  const terminalMsg = !tmuxOpened
2643
2645
  ? isWin
2644
- ? `\n⚠️ Windows Terminal 찾을 없어 모니터 터미널 미생성`
2645
- : `\n⚠️ tmux 찾을 없어 모니터 터미널 미생성`
2646
+ ? `\n⚠️ Windows Terminal not found, monitor terminal not created`
2647
+ : `\n⚠️ tmux not found, monitor terminal not created`
2646
2648
  : physicalOpened
2647
2649
  ? isWin
2648
- ? `\n🖥️ 모니터 터미널 오픈됨 (Windows Terminal)`
2649
- : `\n🖥️ 모니터 터미널 오픈됨: tmux new-session -t ${TMUX_SESSION}`
2650
+ ? `\n🖥️ Monitor terminal opened (Windows Terminal)`
2651
+ : `\n🖥️ Monitor terminal opened: tmux new-session -t ${TMUX_SESSION}`
2650
2652
  : isWin
2651
- ? `\n⚠️ 모니터 터미널 자동 오픈 실패`
2652
- : `\n⚠️ tmux 윈도우는 생성됐지만 외부 터미널 자동 오픈 실패. 수동 실행: tmux new-session -t ${TMUX_SESSION}`;
2653
+ ? `\n⚠️ Monitor terminal auto-open failed`
2654
+ : `\n⚠️ tmux window created but external terminal auto-open failed. Manual run: tmux new-session -t ${TMUX_SESSION}`;
2653
2655
  const manualNotDetected = hasManualSpeakers
2654
2656
  ? speakerOrder.filter(s => !candidateSnapshot.candidates.some(c => c.speaker === s))
2655
2657
  : [];
2656
2658
  const detectWarning = manualNotDetected.length > 0
2657
- ? `\n\n⚠️ 현재 환경에서 즉시 검출되지 않은 speaker: ${manualNotDetected.join(", ")}\n(수동 지정으로는 참가 가능)`
2659
+ ? `\n\n⚠️ Speakers not immediately detected in current environment: ${manualNotDetected.join(", ")}\n(Can still participate via manual specification)`
2658
2660
  : "";
2659
2661
 
2660
2662
  const transportSummary = state.participant_profiles.map(p => {
@@ -2666,7 +2668,7 @@ server.tool(
2666
2668
  return {
2667
2669
  content: [{
2668
2670
  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}")\``,
2671
+ 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
2672
  }],
2671
2673
  };
2672
2674
  })
@@ -2674,10 +2676,10 @@ server.tool(
2674
2676
 
2675
2677
  server.tool(
2676
2678
  "deliberation_speaker_candidates",
2677
- "사용자가 선택 가능한 스피커 후보(로컬 CLI + 브라우저 LLM )를 조회합니다.",
2679
+ "Query available speaker candidates (local CLI + browser LLM tabs).",
2678
2680
  {
2679
- include_cli: z.boolean().default(true).describe("로컬 CLI 후보 포함"),
2680
- include_browser: z.boolean().default(true).describe("브라우저 LLM 후보 포함"),
2681
+ include_cli: z.boolean().default(true).describe("Include local CLI candidates"),
2682
+ include_browser: z.boolean().default(true).describe("Include browser LLM tab candidates"),
2681
2683
  },
2682
2684
  async ({ include_cli, include_browser }) => {
2683
2685
  const snapshot = await collectSpeakerCandidates({ include_cli, include_browser });
@@ -2688,17 +2690,17 @@ server.tool(
2688
2690
 
2689
2691
  server.tool(
2690
2692
  "deliberation_list_active",
2691
- "현재 프로젝트에서 진행 중인 모든 deliberation 세션 목록을 반환합니다.",
2693
+ "List all active deliberation sessions in the current project.",
2692
2694
  {},
2693
2695
  async () => {
2694
2696
  const active = listActiveSessions();
2695
2697
  if (active.length === 0) {
2696
- return { content: [{ type: "text", text: "진행 중인 deliberation이 없습니다." }] };
2698
+ return { content: [{ type: "text", text: t("No active deliberations.", "진행 중인 deliberation이 없습니다.", "en") }] };
2697
2699
  }
2698
2700
 
2699
- let list = `## 진행 중인 Deliberation (${getProjectSlug()}) — ${active.length}개\n\n`;
2701
+ let list = `## Active Deliberations (${getProjectSlug()}) — ${active.length}\n\n`;
2700
2702
  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`;
2703
+ 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
2704
  }
2703
2705
  return { content: [{ type: "text", text: list }] };
2704
2706
  }
@@ -2706,14 +2708,14 @@ server.tool(
2706
2708
 
2707
2709
  server.tool(
2708
2710
  "deliberation_status",
2709
- "deliberation 상태를 조회합니다. 활성 세션이 1개면 자동 선택, 여러 개면 session_id 필요.",
2711
+ "Query deliberation status. Auto-selects if only one active session, requires session_id for multiple.",
2710
2712
  {
2711
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
2713
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
2712
2714
  },
2713
2715
  async ({ session_id }) => {
2714
2716
  const resolved = resolveSessionId(session_id);
2715
2717
  if (!resolved) {
2716
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다. deliberation_start로 시작하세요." }] };
2718
+ return { content: [{ type: "text", text: t("No active deliberation. Start one with deliberation_start.", "활성 deliberation이 없습니다. deliberation_start로 시작하세요.", "en") }] };
2717
2719
  }
2718
2720
  if (resolved === "MULTIPLE") {
2719
2721
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2721,13 +2723,13 @@ server.tool(
2721
2723
 
2722
2724
  const state = loadSession(resolved);
2723
2725
  if (!state) {
2724
- return { content: [{ type: "text", text: `세션 "${resolved}"을 찾을 수 없습니다.` }] };
2726
+ return { content: [{ type: "text", text: t(`Session "${resolved}" not found.`, `세션 "${resolved}"을 찾을 수 없습니다.`, "en") }] };
2725
2727
  }
2726
2728
 
2727
2729
  return {
2728
2730
  content: [{
2729
2731
  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)}` : ""}`,
2732
+ 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
2733
  }],
2732
2734
  };
2733
2735
  }
@@ -2735,7 +2737,7 @@ server.tool(
2735
2737
 
2736
2738
  server.tool(
2737
2739
  "deliberation_context",
2738
- "현재 프로젝트의 컨텍스트(md 파일들) 로드합니다. CWD + Obsidian 자동 감지.",
2740
+ "Load project context (markdown files). Auto-detects CWD + Obsidian.",
2739
2741
  {},
2740
2742
  async () => {
2741
2743
  const dirs = detectContextDirs();
@@ -2743,7 +2745,7 @@ server.tool(
2743
2745
  return {
2744
2746
  content: [{
2745
2747
  type: "text",
2746
- text: `## 프로젝트 컨텍스트 (${getProjectSlug()})\n\n**소스:** ${dirs.join(", ")}\n\n${context}`,
2748
+ text: `## Project Context (${getProjectSlug()})\n\n**Source:** ${dirs.join(", ")}\n\n${context}`,
2747
2749
  }],
2748
2750
  };
2749
2751
  }
@@ -2751,13 +2753,13 @@ server.tool(
2751
2753
 
2752
2754
  server.tool(
2753
2755
  "deliberation_browser_llm_tabs",
2754
- "현재 브라우저에서 열려 있는 LLM (chatgpt/claude/gemini )을 조회합니다.",
2756
+ "Query LLM tabs currently open in the browser (chatgpt/claude/gemini etc).",
2755
2757
  {},
2756
2758
  async () => {
2757
2759
  const { tabs, note } = await collectBrowserLlmTabs();
2758
2760
  if (tabs.length === 0) {
2759
2761
  const suffix = note ? `\n\n${note}` : "";
2760
- return { content: [{ type: "text", text: `감지된 LLM 탭이 없습니다.${suffix}` }] };
2762
+ return { content: [{ type: "text", text: t(`No LLM tabs detected.${suffix}`, `감지된 LLM 탭이 없습니다.${suffix}`, "en") }] };
2761
2763
  }
2762
2764
 
2763
2765
  const lines = tabs.map((t, i) => `${i + 1}. [${t.browser}] ${t.title}\n ${t.url}`).join("\n");
@@ -2768,17 +2770,17 @@ server.tool(
2768
2770
 
2769
2771
  server.tool(
2770
2772
  "deliberation_route_turn",
2771
- "현재 턴의 speaker에 맞는 transport 자동 결정하고 안내합니다. CLI speaker 자동 응답 경로, 브라우저 speaker는 클립보드 경로로 라우팅합니다.",
2773
+ "Auto-determine and guide the transport for the current turn's speaker. Routes CLI speakers to auto-response and browser speakers to clipboard.",
2772
2774
  {
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("프롬프트에 포함할 최근 로그 개수"),
2775
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
2776
+ auto_prepare_clipboard: z.boolean().default(true).describe("Auto-run clipboard prepare for browser speakers"),
2777
+ prompt: z.string().optional().describe("Additional instructions to pass to browser LLM"),
2778
+ include_history_entries: z.number().int().min(0).max(12).default(4).describe("Number of recent log entries to include in prompt"),
2777
2779
  },
2778
2780
  safeToolHandler("deliberation_route_turn", async ({ session_id, auto_prepare_clipboard, prompt, include_history_entries }) => {
2779
2781
  const resolved = resolveSessionId(session_id);
2780
2782
  if (!resolved) {
2781
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
2783
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
2782
2784
  }
2783
2785
  if (resolved === "MULTIPLE") {
2784
2786
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2786,7 +2788,7 @@ server.tool(
2786
2788
 
2787
2789
  const state = loadSession(resolved);
2788
2790
  if (!state || state.status !== "active") {
2789
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
2791
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
2790
2792
  }
2791
2793
 
2792
2794
  const speaker = state.current_speaker;
@@ -2802,9 +2804,9 @@ server.tool(
2802
2804
 
2803
2805
  let guidance;
2804
2806
  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 사용하세요.`;
2807
+ guidance = `🟢 **It's your turn.** You (${speaker}) are the current speaker.\n\n` +
2808
+ `Write your response and submit via \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`.\n\n` +
2809
+ `⚠️ **Do not use cli_auto_turn**: Recursively calling yourself will cause a timeout. You must use deliberation_respond directly.`;
2808
2810
  } else {
2809
2811
  guidance = formatTransportGuidance(transport, state, speaker);
2810
2812
  }
@@ -2854,25 +2856,25 @@ server.tool(
2854
2856
  channel_used: "browser_auto",
2855
2857
  fallback_reason: null,
2856
2858
  });
2857
- const routeModelInfo = modelSelection.model !== 'default' ? ` | 모델: ${modelSelection.model}` : "";
2858
- extra = `\n\n⚡ 자동 실행 완료! 브라우저 LLM 응답이 자동으로 제출되었습니다. (${waitResult.data.elapsedMs}ms${routeModelInfo})`;
2859
+ const routeModelInfo = modelSelection.model !== 'default' ? ` | model: ${modelSelection.model}` : "";
2860
+ extra = `\n\n⚡ Auto-execution complete! Browser LLM response was automatically submitted. (${waitResult.data.elapsedMs}ms${routeModelInfo})`;
2859
2861
  } else {
2860
2862
  throw new Error(waitResult.error?.message || "no response received");
2861
2863
  }
2862
2864
  } catch (autoErr) {
2863
2865
  const errMsg = autoErr instanceof Error ? autoErr.message : String(autoErr);
2864
- extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). Chrome --remote-debugging-port=9222로 재시작하세요.`;
2866
+ extra = `\n\n⚠️ Auto-execution failed (${errMsg}). Restart Chrome with --remote-debugging-port=9222.`;
2865
2867
  }
2866
2868
  }
2867
2869
 
2868
2870
  const profileInfo = profile
2869
- ? `\n**프로필:** ${profile.type}${profile.url ? ` | ${profile.url}` : ""}${profile.command ? ` | command: ${profile.command}` : ""}`
2871
+ ? `\n**Profile:** ${profile.type}${profile.url ? ` | ${profile.url}` : ""}${profile.command ? ` | command: ${profile.command}` : ""}`
2870
2872
  : "";
2871
2873
 
2872
2874
  return {
2873
2875
  content: [{
2874
2876
  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}`,
2877
+ 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
2878
  }],
2877
2879
  };
2878
2880
  })
@@ -2880,16 +2882,16 @@ server.tool(
2880
2882
 
2881
2883
  server.tool(
2882
2884
  "deliberation_browser_auto_turn",
2883
- "브라우저 LLM 자동으로 턴을 전송하고 응답을 수집합니다 (CDP 기반).",
2885
+ "Automatically send a turn to a browser LLM and collect the response (CDP-based).",
2884
2886
  {
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("응답 대기 타임아웃 ()"),
2887
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
2888
+ provider: z.string().optional().default("chatgpt").describe("LLM provider (chatgpt, claude, gemini)"),
2889
+ timeout_sec: z.number().optional().default(45).describe("Response wait timeout (seconds)"),
2888
2890
  },
2889
2891
  safeToolHandler("deliberation_browser_auto_turn", async ({ session_id, provider, timeout_sec }) => {
2890
2892
  const resolved = resolveSessionId(session_id);
2891
2893
  if (!resolved) {
2892
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
2894
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
2893
2895
  }
2894
2896
  if (resolved === "MULTIPLE") {
2895
2897
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -2897,17 +2899,17 @@ server.tool(
2897
2899
 
2898
2900
  const state = loadSession(resolved);
2899
2901
  if (!state || state.status !== "active") {
2900
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
2902
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
2901
2903
  }
2902
2904
 
2903
2905
  const speaker = state.current_speaker;
2904
2906
  if (speaker === "none") {
2905
- return { content: [{ type: "text", text: "현재 발언 차례인 speaker가 없습니다." }] };
2907
+ return { content: [{ type: "text", text: t("No speaker currently has the turn.", "현재 발언 차례인 speaker가 없습니다.", state?.lang) }] };
2906
2908
  }
2907
2909
 
2908
2910
  const { transport } = resolveTransportForSpeaker(state, speaker);
2909
2911
  if (transport !== "browser_auto" && transport !== "clipboard") {
2910
- return { content: [{ type: "text", text: `speaker "${speaker}"는 브라우저 타입이 아닙니다 (transport: ${transport}). CLI speaker는 deliberation_respond를 사용하세요.` }] };
2912
+ 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
2913
  }
2912
2914
 
2913
2915
  const turnId = state.pending_turn_id || generateTurnId();
@@ -2929,14 +2931,14 @@ server.tool(
2929
2931
  };
2930
2932
  const attachResult = await port.attach(resolved, attachHint);
2931
2933
  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}` }] };
2934
+ 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
2935
  }
2934
2936
 
2935
2937
  // Step 1.2: Login detection — check if user is logged in to the web LLM
2936
2938
  const loginCheck = await port.checkLogin(resolved);
2937
2939
  if (loginCheck && !loginCheck.loggedIn) {
2938
2940
  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는 건너뛰는 것이 올바른 동작입니다.` }] };
2941
+ 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
2942
  }
2941
2943
 
2942
2944
  // Step 1.5: Switch model based on context analysis
@@ -2954,7 +2956,7 @@ server.tool(
2954
2956
  return submitDeliberationTurn({
2955
2957
  session_id: resolved,
2956
2958
  speaker,
2957
- content: `[browser_auto 실패 — fallback] ${sendResult.error.message}`,
2959
+ content: `[browser_auto failed — fallback] ${sendResult.error.message}`,
2958
2960
  turn_id: turnId,
2959
2961
  channel_used: "browser_auto_fallback",
2960
2962
  fallback_reason: sendResult.error.code,
@@ -2964,7 +2966,7 @@ server.tool(
2964
2966
  // Step 4: Wait for response
2965
2967
  const waitResult = await port.waitTurnResult(resolved, turnId, timeout_sec);
2966
2968
  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}` }] };
2969
+ 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
2970
  }
2969
2971
 
2970
2972
  // Step 5: Submit the response
@@ -2987,13 +2989,13 @@ server.tool(
2987
2989
  : "";
2988
2990
 
2989
2991
  const modelInfo = modelSelection.model !== 'default'
2990
- ? `\n**모델:** ${modelSelection.model} (${modelSelection.reason})\n**분석:** category=${modelSelection.category}, complexity=${modelSelection.complexity}`
2992
+ ? `\n**Model:** ${modelSelection.model} (${modelSelection.reason})\n**Analysis:** category=${modelSelection.category}, complexity=${modelSelection.complexity}`
2991
2993
  : "";
2992
2994
 
2993
2995
  return {
2994
2996
  content: [{
2995
2997
  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}`,
2998
+ 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
2999
  }],
2998
3000
  };
2999
3001
  })
@@ -3001,15 +3003,15 @@ server.tool(
3001
3003
 
3002
3004
  server.tool(
3003
3005
  "deliberation_cli_auto_turn",
3004
- "CLI speaker 자동으로 턴을 전송하고 응답을 수집합니다.",
3006
+ "Automatically send a turn to a CLI speaker and collect the response.",
3005
3007
  {
3006
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
3007
- timeout_sec: z.number().optional().default(120).describe("CLI 응답 대기 타임아웃 ()"),
3008
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3009
+ timeout_sec: z.number().optional().default(120).describe("CLI response wait timeout (seconds)"),
3008
3010
  },
3009
3011
  safeToolHandler("deliberation_cli_auto_turn", async ({ session_id, timeout_sec }) => {
3010
3012
  const resolved = resolveSessionId(session_id);
3011
3013
  if (!resolved) {
3012
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
3014
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
3013
3015
  }
3014
3016
  if (resolved === "MULTIPLE") {
3015
3017
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -3017,27 +3019,27 @@ server.tool(
3017
3019
 
3018
3020
  const state = loadSession(resolved);
3019
3021
  if (!state || state.status !== "active") {
3020
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
3022
+ return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
3021
3023
  }
3022
3024
 
3023
3025
  const speaker = state.current_speaker;
3024
3026
  if (speaker === "none") {
3025
- return { content: [{ type: "text", text: "현재 발언 차례인 speaker가 없습니다." }] };
3027
+ return { content: [{ type: "text", text: t("No speaker currently has the turn.", "현재 발언 차례인 speaker가 없습니다.", state?.lang) }] };
3026
3028
  }
3027
3029
 
3028
3030
  const { transport } = resolveTransportForSpeaker(state, speaker);
3029
3031
  if (transport !== "cli_respond") {
3030
- return { content: [{ type: "text", text: `speaker "${speaker}"는 CLI 타입이 아닙니다 (transport: ${transport}). 브라우저 speaker는 deliberation_browser_auto_turn을 사용하세요.` }] };
3032
+ 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
3033
  }
3032
3034
 
3033
3035
  // Block recursive self-spawn: if the speaker is the same CLI as the caller,
3034
3036
  // spawning it would create infinite recursion and timeout.
3035
3037
  const callerSpeaker = detectCallerSpeaker();
3036
3038
  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: "...")\`로 제출하세요.`
3039
+ return { content: [{ type: "text", text: t(
3040
+ `⚠️ **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: "...")\`.`,
3041
+ `⚠️ **재귀 호출 차단**: speaker "${speaker}"는 현재 오케스트레이터와 동일한 CLI입니다.\n\ncli_auto_turn으로 자기 자신을 spawn하면 타임아웃이 발생합니다.\n직접 응답을 작성하여 \`deliberation_respond(session_id: "${resolved}", speaker: "${speaker}", content: "...")\`로 제출하세요.`,
3042
+ state?.lang)
3041
3043
  }] };
3042
3044
  }
3043
3045
 
@@ -3047,12 +3049,12 @@ server.tool(
3047
3049
 
3048
3050
  const hint = CLI_INVOCATION_HINTS[speaker];
3049
3051
  if (!hint) {
3050
- return { content: [{ type: "text", text: `speaker "${speaker}"에 대한 CLI 호출 정보가 없습니다. CLI_INVOCATION_HINTS에 등록되지 않은 speaker입니다.` }] };
3052
+ 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
3053
  }
3052
3054
 
3053
3055
  // Check CLI liveness
3054
3056
  if (!checkCliLiveness(hint.cmd)) {
3055
- return { content: [{ type: "text", text: `❌ CLI "${hint.cmd}"가 설치되어 있지 않거나 실행할 수 없습니다.` }] };
3057
+ return { content: [{ type: "text", text: t(`❌ CLI "${hint.cmd}" is not installed or cannot be executed.`, `❌ CLI "${hint.cmd}"가 설치되어 있지 않거나 실행할 수 없습니다.`, state?.lang) }] };
3056
3058
  }
3057
3059
 
3058
3060
  const turnId = state.pending_turn_id || generateTurnId();
@@ -3095,7 +3097,7 @@ server.tool(
3095
3097
 
3096
3098
  const timer = setTimeout(() => {
3097
3099
  child.kill("SIGTERM");
3098
- reject(new Error(`CLI 타임아웃 (${effectiveTimeout})`));
3100
+ reject(new Error(`CLI timeout (${effectiveTimeout}s)`));
3099
3101
  }, effectiveTimeout * 1000);
3100
3102
 
3101
3103
  child.stdout.on("data", (data) => { stdout += data.toString(); });
@@ -3131,7 +3133,7 @@ server.tool(
3131
3133
  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
3134
 
3133
3135
  if (!response) {
3134
- return { content: [{ type: "text", text: `⚠️ CLI "${speaker}"가 빈 응답을 반환했습니다.` }] };
3136
+ return { content: [{ type: "text", text: t(`⚠️ CLI "${speaker}" returned an empty response.`, `⚠️ CLI "${speaker}"가 빈 응답을 반환했습니다.`, state?.lang) }] };
3135
3137
  }
3136
3138
 
3137
3139
  // Submit the response
@@ -3147,7 +3149,7 @@ server.tool(
3147
3149
  return {
3148
3150
  content: [{
3149
3151
  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}`,
3152
+ 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
3153
  }],
3152
3154
  };
3153
3155
 
@@ -3155,7 +3157,7 @@ server.tool(
3155
3157
  return {
3156
3158
  content: [{
3157
3159
  type: "text",
3158
- text: `❌ CLI 자동 실패: ${err.message}\n\n**Speaker:** ${speaker}\n**CLI:** ${hint.cmd}\n\ndeliberation_respond(speaker: "${speaker}", content: "...")로 수동 응답을 제출할 수 있습니다.`,
3160
+ 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
3161
  }],
3160
3162
  };
3161
3163
  }
@@ -3164,13 +3166,13 @@ server.tool(
3164
3166
 
3165
3167
  server.tool(
3166
3168
  "deliberation_respond",
3167
- "현재 턴의 응답을 제출합니다.",
3169
+ "Submit a response for the current turn.",
3168
3170
  {
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에서 받은 )"),
3171
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3172
+ speaker: z.string().trim().min(1).max(64).describe("Responder name"),
3173
+ content: z.string().optional().describe("Response content (markdown). Either content or content_file is required."),
3174
+ content_file: z.string().optional().describe("File path containing response content. For avoiding JSON escape issues. File content is used as-is for content."),
3175
+ turn_id: z.string().optional().describe("Turn verification ID (value received from deliberation_route_turn)"),
3174
3176
  },
3175
3177
  safeToolHandler("deliberation_respond", async ({ session_id, speaker, content, content_file, turn_id }) => {
3176
3178
  // Guard: prevent orchestrator from fabricating responses for CLI/browser speakers
@@ -3187,12 +3189,10 @@ server.tool(
3187
3189
  return {
3188
3190
  content: [{
3189
3191
  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/브라우저를 실행하여 진짜 응답을 수집합니다.`,
3192
+ text: t(
3193
+ `⚠️ **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.`,
3194
+ `⚠️ **대리 응답 차단**: 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/브라우저를 실행하여 진짜 응답을 수집합니다.`,
3195
+ state?.lang),
3196
3196
  }],
3197
3197
  };
3198
3198
  }
@@ -3206,11 +3206,11 @@ server.tool(
3206
3206
  try {
3207
3207
  finalContent = fs.readFileSync(content_file, "utf-8").trim();
3208
3208
  } catch (e) {
3209
- return { content: [{ type: "text", text: `❌ content_file 읽기 실패: ${e.message}` }] };
3209
+ return { content: [{ type: "text", text: t(`❌ Failed to read content_file: ${e.message}`, `❌ content_file 읽기 실패: ${e.message}`, state?.lang) }] };
3210
3210
  }
3211
3211
  }
3212
3212
  if (!finalContent) {
3213
- return { content: [{ type: "text", text: "❌ content 또는 content_file 중 하나를 제공해야 합니다." }] };
3213
+ return { content: [{ type: "text", text: t("❌ Either content or content_file must be provided.", "❌ content 또는 content_file 중 하나를 제공해야 합니다.", "en") }] };
3214
3214
  }
3215
3215
  return submitDeliberationTurn({ session_id, speaker, content: finalContent, turn_id, channel_used: "cli_respond" });
3216
3216
  })
@@ -3218,14 +3218,14 @@ server.tool(
3218
3218
 
3219
3219
  server.tool(
3220
3220
  "deliberation_history",
3221
- "토론 기록을 반환합니다.",
3221
+ "Return the deliberation history.",
3222
3222
  {
3223
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
3223
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3224
3224
  },
3225
3225
  async ({ session_id }) => {
3226
3226
  const resolved = resolveSessionId(session_id);
3227
3227
  if (!resolved) {
3228
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
3228
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
3229
3229
  }
3230
3230
  if (resolved === "MULTIPLE") {
3231
3231
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -3233,19 +3233,19 @@ server.tool(
3233
3233
 
3234
3234
  const state = loadSession(resolved);
3235
3235
  if (!state) {
3236
- return { content: [{ type: "text", text: `세션 "${resolved}"을 찾을 수 없습니다.` }] };
3236
+ return { content: [{ type: "text", text: t(`Session "${resolved}" not found.`, `세션 "${resolved}"을 찾을 수 없습니다.`, "en") }] };
3237
3237
  }
3238
3238
 
3239
3239
  if (state.log.length === 0) {
3240
3240
  return {
3241
3241
  content: [{
3242
3242
  type: "text",
3243
- text: `**세션:** ${state.id}\n**주제:** ${state.topic}\n\n아직 응답이 없습니다. **${state.current_speaker}**가 먼저 응답하세요.`,
3243
+ 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
3244
  }],
3245
3245
  };
3246
3246
  }
3247
3247
 
3248
- let history = `**세션:** ${state.id}\n**주제:** ${state.topic} | **상태:** ${state.status}\n\n`;
3248
+ let history = `**Session:** ${state.id}\n**Topic:** ${state.topic} | **Status:** ${state.status}\n\n`;
3249
3249
  for (const e of state.log) {
3250
3250
  history += `### ${e.speaker} — Round ${e.round}\n\n${e.content}\n\n---\n\n`;
3251
3251
  }
@@ -3255,15 +3255,15 @@ server.tool(
3255
3255
 
3256
3256
  server.tool(
3257
3257
  "deliberation_synthesize",
3258
- "토론을 종료하고 합성 보고서를 제출합니다.",
3258
+ "End the deliberation and submit a synthesis report.",
3259
3259
  {
3260
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
3261
- synthesis: z.string().describe("합성 보고서 (마크다운)"),
3260
+ session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
3261
+ synthesis: z.string().describe("Synthesis report (markdown)"),
3262
3262
  },
3263
3263
  safeToolHandler("deliberation_synthesize", async ({ session_id, synthesis }) => {
3264
3264
  const resolved = resolveSessionId(session_id);
3265
3265
  if (!resolved) {
3266
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
3266
+ return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
3267
3267
  }
3268
3268
  if (resolved === "MULTIPLE") {
3269
3269
  return { content: [{ type: "text", text: multipleSessionsError() }] };
@@ -3274,7 +3274,7 @@ server.tool(
3274
3274
  const lockedResult = withSessionLock(resolved, () => {
3275
3275
  const loaded = loadSession(resolved);
3276
3276
  if (!loaded) {
3277
- return { content: [{ type: "text", text: `세션 "${resolved}"을 찾을 수 없습니다.` }] };
3277
+ return { content: [{ type: "text", text: t(`Session "${resolved}" not found.`, `세션 "${resolved}"을 찾을 수 없습니다.`, "en") }] };
3278
3278
  }
3279
3279
 
3280
3280
  loaded.synthesis = synthesis;
@@ -3292,13 +3292,13 @@ server.tool(
3292
3292
 
3293
3293
  appendRuntimeLog("INFO", `SYNTHESIZED: ${resolved} | turns: ${state.log.length} | rounds: ${state.max_rounds}`);
3294
3294
 
3295
- // 토론 종료 즉시 모니터 터미널(물리 Terminal 포함) 강제 종료
3295
+ // Immediately force-close monitor terminal (including physical Terminal) on deliberation end
3296
3296
  closeMonitorTerminal(state.id, getSessionWindowIds(state));
3297
3297
 
3298
3298
  return {
3299
3299
  content: [{
3300
3300
  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🖥️ 모니터 터미널이 즉시 강제 종료되었습니다.`,
3301
+ 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
3302
  }],
3303
3303
  };
3304
3304
  })
@@ -3306,13 +3306,13 @@ server.tool(
3306
3306
 
3307
3307
  server.tool(
3308
3308
  "deliberation_list",
3309
- "과거 deliberation 아카이브 목록을 반환합니다.",
3309
+ "Return the list of past deliberation archives.",
3310
3310
  {},
3311
3311
  async () => {
3312
3312
  ensureDirs();
3313
3313
  const archiveDir = getArchiveDir();
3314
3314
  if (!fs.existsSync(archiveDir)) {
3315
- return { content: [{ type: "text", text: "과거 deliberation이 없습니다." }] };
3315
+ return { content: [{ type: "text", text: t("No past deliberations.", "과거 deliberation이 없습니다.", "en") }] };
3316
3316
  }
3317
3317
 
3318
3318
  const files = fs.readdirSync(archiveDir)
@@ -3320,31 +3320,31 @@ server.tool(
3320
3320
  .sort().reverse();
3321
3321
 
3322
3322
  if (files.length === 0) {
3323
- return { content: [{ type: "text", text: "과거 deliberation이 없습니다." }] };
3323
+ return { content: [{ type: "text", text: t("No past deliberations.", "과거 deliberation이 없습니다.", "en") }] };
3324
3324
  }
3325
3325
 
3326
3326
  const list = files.map((f, i) => `${i + 1}. ${f.replace(".md", "")}`).join("\n");
3327
- return { content: [{ type: "text", text: `## 과거 Deliberation (${getProjectSlug()})\n\n${list}` }] };
3327
+ return { content: [{ type: "text", text: `## Past Deliberations (${getProjectSlug()})\n\n${list}` }] };
3328
3328
  }
3329
3329
  );
3330
3330
 
3331
3331
  server.tool(
3332
3332
  "deliberation_reset",
3333
- "deliberation 초기화합니다. session_id 지정 해당 세션만, 미지정 전체 초기화.",
3333
+ "Reset deliberation. Resets specific session if session_id provided, otherwise resets all.",
3334
3334
  {
3335
- session_id: z.string().optional().describe("초기화할 세션 ID (미지정 전체 초기화)"),
3335
+ session_id: z.string().optional().describe("Session ID to reset (resets all if omitted)"),
3336
3336
  },
3337
3337
  safeToolHandler("deliberation_reset", async ({ session_id }) => {
3338
3338
  ensureDirs();
3339
3339
  const sessionsDir = getSessionsDir();
3340
3340
 
3341
3341
  if (session_id) {
3342
- // 특정 세션만 초기화
3342
+ // Reset specific session only
3343
3343
  let toCloseIds = [];
3344
3344
  const result = withSessionLock(session_id, () => {
3345
3345
  const file = getSessionFile(session_id);
3346
3346
  if (!fs.existsSync(file)) {
3347
- return { content: [{ type: "text", text: `세션 "${session_id}"을 찾을 수 없습니다.` }] };
3347
+ return { content: [{ type: "text", text: t(`Session "${session_id}" not found.`, `세션 "${session_id}"을 찾을 수 없습니다.`, "en") }] };
3348
3348
  }
3349
3349
  const state = loadSession(session_id);
3350
3350
  if (state && state.log.length > 0) {
@@ -3353,7 +3353,7 @@ server.tool(
3353
3353
  if (state) cleanupSyncMarkdown(state);
3354
3354
  toCloseIds = getSessionWindowIds(state);
3355
3355
  fs.unlinkSync(file);
3356
- return { content: [{ type: "text", text: `✅ 세션 "${session_id}" 초기화 완료. 🖥️ 모니터 터미널 닫힘.` }] };
3356
+ return { content: [{ type: "text", text: t(`✅ Session "${session_id}" reset complete. 🖥️ Monitor terminal closed.`, `✅ 세션 "${session_id}" 초기화 완료. 🖥️ 모니터 터미널 닫힘.`, "en") }] };
3357
3357
  });
3358
3358
  if (toCloseIds.length > 0) {
3359
3359
  closeMonitorTerminal(session_id, toCloseIds);
@@ -3361,7 +3361,7 @@ server.tool(
3361
3361
  return result;
3362
3362
  }
3363
3363
 
3364
- // 전체 초기화
3364
+ // Reset all
3365
3365
  const resetResult = withProjectLock(() => {
3366
3366
  if (!fs.existsSync(sessionsDir)) {
3367
3367
  return { files: [], archived: 0, terminalWindowIds: [], noSessions: true };
@@ -3397,7 +3397,7 @@ server.tool(
3397
3397
  });
3398
3398
 
3399
3399
  if (resetResult.noSessions) {
3400
- return { content: [{ type: "text", text: "초기화할 세션이 없습니다." }] };
3400
+ return { content: [{ type: "text", text: t("No sessions to reset.", "초기화할 세션이 없습니다.", "en") }] };
3401
3401
  }
3402
3402
 
3403
3403
  for (const windowId of resetResult.terminalWindowIds) {
@@ -3408,7 +3408,7 @@ server.tool(
3408
3408
  return {
3409
3409
  content: [{
3410
3410
  type: "text",
3411
- text: `✅ 전체 초기화 완료. ${resetResult.files.length} 세션 삭제, ${resetResult.archived} 아카이브됨. 🖥️ 모든 모니터 터미널 닫힘.`,
3411
+ text: `✅ Full reset complete. ${resetResult.files.length} sessions deleted, ${resetResult.archived} archived. 🖥️ All monitor terminals closed.`,
3412
3412
  }],
3413
3413
  };
3414
3414
  })
@@ -3416,17 +3416,17 @@ server.tool(
3416
3416
 
3417
3417
  server.tool(
3418
3418
  "deliberation_cli_config",
3419
- "딜리버레이션 참가자 CLI 설정을 조회하거나 변경합니다. enabled_clis 지정하면 저장합니다.",
3419
+ "Query or update deliberation participant CLI settings. Saves when enabled_clis is provided.",
3420
3420
  {
3421
- enabled_clis: z.array(z.string()).optional().describe("활성화할 CLI 목록 (예: [\"claude\", \"codex\", \"gemini\"]). 미지정 현재 설정 조회"),
3421
+ enabled_clis: z.array(z.string()).optional().describe("CLI list to enable (e.g., [\"claude\", \"codex\", \"gemini\"]). Shows current settings if omitted"),
3422
3422
  require_speaker_selection: z.preprocess(
3423
3423
  (v) => (typeof v === "string" ? v === "true" : v),
3424
3424
  z.boolean().optional()
3425
- ).describe("true: 매번 사용자가 스피커 선택 시작, false: 감지된 스피커 전체 자동 참여"),
3425
+ ).describe("true: user selects speakers before each start, false: all detected speakers auto-join"),
3426
3426
  default_rounds: z.coerce.number().int().min(1).max(10).optional()
3427
- .describe("기본 라운드 (1-10, 기본 3)"),
3427
+ .describe("Default number of rounds (1-10, default 3)"),
3428
3428
  default_ordering: z.enum(["auto", "cyclic", "random", "weighted-random"]).optional()
3429
- .describe("기본 발언 순서 전략: auto(스피커 수에 따라 자동), cyclic, random, weighted-random"),
3429
+ .describe("Default ordering strategy: auto (automatic based on speaker count), cyclic, random, weighted-random"),
3430
3430
  },
3431
3431
  safeToolHandler("deliberation_cli_config", async ({ enabled_clis, require_speaker_selection, default_rounds, default_ordering }) => {
3432
3432
  const config = loadDeliberationConfig();
@@ -3459,7 +3459,7 @@ server.tool(
3459
3459
  return {
3460
3460
  content: [{
3461
3461
  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: [])\``,
3462
+ 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**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 revert to full auto-detection:\n\`deliberation_cli_config(enabled_clis: [])\``,
3463
3463
  }],
3464
3464
  };
3465
3465
  }
@@ -3472,7 +3472,7 @@ server.tool(
3472
3472
  return {
3473
3473
  content: [{
3474
3474
  type: "text",
3475
- text: `✅ CLI 설정 초기화 완료. 전체 자동 감지 모드로 전환되었습니다.\n감지 대상: ${DEFAULT_CLI_CANDIDATES.join(", ")}`,
3475
+ text: `✅ CLI settings reset. Switched to full auto-detection mode.\nDetection targets: ${DEFAULT_CLI_CANDIDATES.join(", ")}`,
3476
3476
  }],
3477
3477
  };
3478
3478
  }
@@ -3497,9 +3497,9 @@ server.tool(
3497
3497
  });
3498
3498
  const notInstalled = valid.filter(cli => !installed.includes(cli));
3499
3499
 
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에서 찾을 없음)`;
3500
+ let result = `✅ CLI settings saved!\n\n**Enabled CLIs:** ${valid.join(", ")}`;
3501
+ if (installed.length > 0) result += `\n**Installed:** ${installed.join(", ")}`;
3502
+ if (notInstalled.length > 0) result += `\n**⚠️ Not installed:** ${notInstalled.join(", ")} (not found in PATH)`;
3503
3503
 
3504
3504
  return { content: [{ type: "text", text: result }] };
3505
3505
  })
@@ -3585,15 +3585,15 @@ function synthesizeReviews(context, question, reviews) {
3585
3585
 
3586
3586
  server.tool(
3587
3587
  "deliberation_request_review",
3588
- "코드 리뷰를 요청합니다. 여러 CLI 리뷰어에게 동시에 리뷰를 요청하고 결과를 종합합니다.",
3588
+ "Request a code review. Sends review requests to multiple CLI reviewers simultaneously and synthesizes results.",
3589
3589
  {
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=에러"),
3590
+ context: z.string().describe("Description of changes to review (code, diff, design, etc.)"),
3591
+ question: z.string().describe("Review question (e.g., 'Is this error handling sufficient?')"),
3592
+ reviewers: z.array(z.string().trim().min(1).max(64)).min(1).describe("Reviewer CLI list (e.g., [\"claude\", \"codex\"])"),
3593
+ mode: z.enum(["sync", "async"]).default("sync").describe("sync: wait for results then return, async: return session_id immediately"),
3594
+ deadline_ms: z.number().int().min(5000).max(600000).default(60000).describe("Total timeout (milliseconds, default 60s)"),
3595
+ min_reviews: z.number().int().min(1).default(1).describe("Minimum required reviews (default 1)"),
3596
+ on_timeout: z.enum(["partial", "fail"]).default("partial").describe("Timeout behavior: partial=return partial results, fail=error"),
3597
3597
  },
3598
3598
  safeToolHandler("deliberation_request_review", async ({ context, question, reviewers, mode, deadline_ms, min_reviews, on_timeout }) => {
3599
3599
  // Validate reviewers exist in PATH
@@ -3613,7 +3613,7 @@ server.tool(
3613
3613
  return {
3614
3614
  content: [{
3615
3615
  type: "text",
3616
- text: `❌ 유효한 리뷰어가 없습니다. PATH에서 찾을 없는 CLI: ${invalidReviewers.join(", ")}\n\n사용 가능한 CLI를 확인하려면 deliberation_speaker_candidates를 호출하세요.`,
3616
+ text: `❌ No valid reviewers. CLIs not found in PATH: ${invalidReviewers.join(", ")}\n\nCall deliberation_speaker_candidates to check available CLIs.`,
3617
3617
  }],
3618
3618
  };
3619
3619
  }
@@ -3657,12 +3657,12 @@ server.tool(
3657
3657
  // Async mode: return immediately
3658
3658
  if (mode === "async") {
3659
3659
  const warn = invalidReviewers.length > 0
3660
- ? `\n⚠️ PATH에서 찾을 없는 리뷰어 (제외됨): ${invalidReviewers.join(", ")}`
3660
+ ? `\n⚠️ Reviewers not found in PATH (excluded): ${invalidReviewers.join(", ")}`
3661
3661
  : "";
3662
3662
  return {
3663
3663
  content: [{
3664
3664
  type: "text",
3665
- text: `✅ 비동기 리뷰 세션 생성됨\n\n**Session ID:** ${sessionId}\n**리뷰어:** ${validReviewers.join(", ")}\n**모드:** async${warn}\n\n진행 상태는 \`deliberation_status(session_id: "${sessionId}")\`로 확인하세요.`,
3665
+ 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
3666
  }],
3667
3667
  };
3668
3668
  }
@@ -3734,7 +3734,7 @@ server.tool(
3734
3734
  return {
3735
3735
  content: [{
3736
3736
  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(", ") || "(없음)"}`,
3737
+ 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
3738
  }],
3739
3739
  };
3740
3740
  }
@@ -3760,13 +3760,13 @@ server.tool(
3760
3760
  const totalMs = Date.now() - globalStart;
3761
3761
  const coverage = `${completedReviews.length}/${validReviewers.length}`;
3762
3762
  const warn = invalidReviewers.length > 0
3763
- ? `\n**제외된 리뷰어 (미설치):** ${invalidReviewers.join(", ")}`
3763
+ ? `\n**Excluded reviewers (not installed):** ${invalidReviewers.join(", ")}`
3764
3764
  : "";
3765
3765
  const timeoutInfo = timedOutReviewers.length > 0
3766
- ? `\n**타임아웃 리뷰어:** ${timedOutReviewers.join(", ")}`
3766
+ ? `\n**Timed out reviewers:** ${timedOutReviewers.join(", ")}`
3767
3767
  : "";
3768
3768
  const failInfo = failedReviewers.length > 0
3769
- ? `\n**실패 리뷰어:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ")}`
3769
+ ? `\n**Failed reviewers:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ")}`
3770
3770
  : "";
3771
3771
 
3772
3772
  const resultPayload = {
@@ -3783,7 +3783,7 @@ server.tool(
3783
3783
  return {
3784
3784
  content: [{
3785
3785
  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\`\`\``,
3786
+ 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
3787
  }],
3788
3788
  };
3789
3789
  })
@@ -3793,22 +3793,22 @@ server.tool(
3793
3793
 
3794
3794
  server.tool(
3795
3795
  "decision_start",
3796
- " 의사결정 세션을 시작합니다. 여러 LLM이 독립적으로 의견을 제시하고 갈등을 가시화합니다.",
3796
+ "Start a new decision session. Multiple LLMs provide independent opinions and conflicts are visualized.",
3797
3797
  {
3798
- problem: z.string().describe("의사결정 문제 (예: 'JWT vs Session 인증 방식 선택')"),
3798
+ problem: z.string().describe("Decision problem (e.g., 'JWT vs Session authentication method')"),
3799
3799
  options: z.preprocess(
3800
3800
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
3801
3801
  z.array(z.string()).optional()
3802
- ).describe("선택지 목록 (예: ['JWT', 'Session', 'OAuth2'])"),
3802
+ ).describe("Options list (e.g., ['JWT', 'Session', 'OAuth2'])"),
3803
3803
  criteria: z.preprocess(
3804
3804
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
3805
3805
  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)"),
3806
+ ).describe("Evaluation criteria (auto-loaded from template if omitted)"),
3807
+ template: z.string().optional().describe("Micro-decision template ID (lib-compare, arch-decision, pr-priority, naming-convention, tradeoff, risk-approval)"),
3808
3808
  speakers: z.preprocess(
3809
3809
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
3810
3810
  z.array(z.string().trim().min(1).max(64)).min(2).optional()
3811
- ).describe("참여 LLM 목록 (최소 2명, 예: ['claude', 'codex', 'gemini'])"),
3811
+ ).describe("Participating LLM list (minimum 2, e.g., ['claude', 'codex', 'gemini'])"),
3812
3812
  },
3813
3813
  safeToolHandler("decision_start", async ({ problem, options, criteria, template, speakers }) => {
3814
3814
  // Auto-discover speakers if not provided
@@ -3819,7 +3819,7 @@ server.tool(
3819
3819
  .map(c => c.speaker)
3820
3820
  .slice(0, 4);
3821
3821
  if (speakers.length < 2) {
3822
- return { content: [{ type: "text", text: "❌ 의사결정에 최소 2명의 speaker가 필요합니다. speakers를 직접 지정하세요." }] };
3822
+ return { content: [{ type: "text", text: t("❌ Decision requires at least 2 speakers. Please specify speakers directly.", "❌ 의사결정에 최소 2명의 speaker가 필요합니다. speakers를 직접 지정하세요.", "en") }] };
3823
3823
  }
3824
3824
  }
3825
3825
 
@@ -3970,7 +3970,7 @@ server.tool(
3970
3970
  return {
3971
3971
  content: [{
3972
3972
  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\`로 제출하세요.`,
3973
+ 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
3974
  }],
3975
3975
  };
3976
3976
  })
@@ -3978,9 +3978,9 @@ server.tool(
3978
3978
 
3979
3979
  server.tool(
3980
3980
  "decision_status",
3981
- "의사결정 세션의 현재 상태를 조회합니다.",
3981
+ "Query the current status of a decision session.",
3982
3982
  {
3983
- session_id: z.string().optional().describe("세션 ID (미지정 활성 decision 세션 자동 선택)"),
3983
+ session_id: z.string().optional().describe("Session ID (auto-selects active decision session if omitted)"),
3984
3984
  },
3985
3985
  safeToolHandler("decision_status", async ({ session_id }) => {
3986
3986
  // Find decision sessions
@@ -3991,13 +3991,13 @@ server.tool(
3991
3991
 
3992
3992
  let resolved = session_id;
3993
3993
  if (!resolved) {
3994
- if (active.length === 0) return { content: [{ type: "text", text: "활성 decision 세션이 없습니다." }] };
3994
+ if (active.length === 0) return { content: [{ type: "text", text: t("No active decision sessions.", "활성 decision 세션이 없습니다.", "en") }] };
3995
3995
  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")}` }] };
3996
+ 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
3997
  }
3998
3998
 
3999
3999
  const state = loadSession(resolved);
4000
- if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
4000
+ if (!state) return { content: [{ type: "text", text: t(`Session not found: ${resolved}`, `세션을 찾을 수 없습니다: ${resolved}`, "en") }] };
4001
4001
 
4002
4002
  const opinionCount = Object.keys(state.opinions || {}).length;
4003
4003
  const conflictCount = (state.conflicts || []).length;
@@ -4015,13 +4015,13 @@ server.tool(
4015
4015
 
4016
4016
  server.tool(
4017
4017
  "decision_respond",
4018
- "user_probe 단계의 갈등 질문에 대한 사용자 응답을 제출합니다.",
4018
+ "Submit user responses to conflict questions in the user_probe stage.",
4019
4019
  {
4020
- session_id: z.string().optional().describe("세션 ID"),
4020
+ session_id: z.string().optional().describe("Session ID"),
4021
4021
  responses: z.preprocess(
4022
4022
  (v) => (typeof v === "string" ? JSON.parse(v) : v),
4023
4023
  z.array(z.string()).min(1)
4024
- ).describe(" 갈등 질문에 대한 응답 배열 (conflict 순서대로)"),
4024
+ ).describe("Response array for each conflict question (in conflict order)"),
4025
4025
  },
4026
4026
  safeToolHandler("decision_respond", async ({ session_id, responses }) => {
4027
4027
  // Find decision session
@@ -4033,8 +4033,8 @@ server.tool(
4033
4033
  let resolved = session_id;
4034
4034
  if (!resolved) {
4035
4035
  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를 지정하세요.` }] };
4036
+ else if (active.length === 0) return { content: [{ type: "text", text: t("No active decision sessions.", "활성 decision 세션이 없습니다.", "en") }] };
4037
+ else return { content: [{ type: "text", text: t(`Multiple decision sessions are active. Please specify session_id.`, `여러 decision 세션이 진행 중입니다. session_id를 지정하세요.`, "en") }] };
4038
4038
  }
4039
4039
 
4040
4040
  let synthesisText = "";
@@ -4044,7 +4044,7 @@ server.tool(
4044
4044
  const state = loadSession(resolved);
4045
4045
  if (!state) return;
4046
4046
  if (state.stage !== "user_probe") {
4047
- synthesisText = `❌ 현재 단계(${state.stage})에서는 응답을 받을 수 없습니다. user_probe 단계에서만 가능합니다.`;
4047
+ synthesisText = t(`❌ Cannot accept responses at current stage (${state.stage}). Only possible during user_probe stage.`, `❌ 현재 단계(${state.stage})에서는 응답을 받을 수 없습니다. user_probe 단계에서만 가능합니다.`, state?.lang);
4048
4048
  return;
4049
4049
  }
4050
4050
 
@@ -4103,9 +4103,9 @@ server.tool(
4103
4103
 
4104
4104
  server.tool(
4105
4105
  "decision_resume",
4106
- "일시 중지된 decision 세션을 재개합니다 (user_probe 단계에서 갈등 질문을 다시 표시).",
4106
+ "Resume a paused decision session (re-displays conflict questions from the user_probe stage).",
4107
4107
  {
4108
- session_id: z.string().optional().describe("세션 ID"),
4108
+ session_id: z.string().optional().describe("Session ID"),
4109
4109
  },
4110
4110
  safeToolHandler("decision_resume", async ({ session_id }) => {
4111
4111
  const active = listActiveSessions().filter(s => {
@@ -4116,21 +4116,21 @@ server.tool(
4116
4116
  let resolved = session_id;
4117
4117
  if (!resolved) {
4118
4118
  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")}` }] };
4119
+ else if (active.length === 0) return { content: [{ type: "text", text: t("No decision sessions to resume.", "재개할 decision 세션이 없습니다.", "en") }] };
4120
+ 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
4121
  }
4122
4122
 
4123
4123
  const state = loadSession(resolved);
4124
- if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
4124
+ if (!state) return { content: [{ type: "text", text: t(`Session not found: ${resolved}`, `세션을 찾을 수 없습니다: ${resolved}`, "en") }] };
4125
4125
  if (state.stage !== "user_probe") {
4126
- return { content: [{ type: "text", text: `세션이 user_probe 단계가 아닙니다 (현재: ${state.stage}). 재개할 수 없습니다.` }] };
4126
+ 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
4127
  }
4128
4128
 
4129
4129
  const conflictText = generateConflictQuestions(state.conflicts || []);
4130
4130
  return {
4131
4131
  content: [{
4132
4132
  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\`로 제출하세요.`,
4133
+ 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
4134
  }],
4135
4135
  };
4136
4136
  })
@@ -4138,14 +4138,14 @@ server.tool(
4138
4138
 
4139
4139
  server.tool(
4140
4140
  "decision_history",
4141
- "과거 의사결정 기록을 조회합니다.",
4141
+ "Query past decision history.",
4142
4142
  {
4143
- session_id: z.string().optional().describe("특정 세션 ID (미지정 전체 목록)"),
4143
+ session_id: z.string().optional().describe("Specific session ID (shows full list if omitted)"),
4144
4144
  },
4145
4145
  safeToolHandler("decision_history", async ({ session_id }) => {
4146
4146
  if (session_id) {
4147
4147
  const state = loadSession(session_id);
4148
- if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${session_id}` }] };
4148
+ if (!state) return { content: [{ type: "text", text: t(`Session not found: ${session_id}`, `세션을 찾을 수 없습니다: ${session_id}`, "en") }] };
4149
4149
 
4150
4150
  const opinionSummary = Object.entries(state.opinions || {})
4151
4151
  .map(([speaker, op]) => `- **${speaker}**: ${op.summary || "(none)"} (confidence: ${Math.round((op.confidence || 0.5) * 100)}%)`)
@@ -4191,12 +4191,12 @@ server.tool(
4191
4191
 
4192
4192
  server.tool(
4193
4193
  "decision_templates",
4194
- "사용 가능한 Micro-Decision 템플릿 목록을 표시합니다.",
4194
+ "Display available Micro-Decision templates.",
4195
4195
  {},
4196
4196
  safeToolHandler("decision_templates", async () => {
4197
4197
  const templates = loadTemplates();
4198
4198
  if (templates.length === 0) {
4199
- return { content: [{ type: "text", text: "사용 가능한 템플릿이 없습니다." }] };
4199
+ return { content: [{ type: "text", text: t("No available templates.", "사용 가능한 템플릿이 없습니다.", "en") }] };
4200
4200
  }
4201
4201
 
4202
4202
  const list = templates.map(t => {
@@ -4207,7 +4207,7 @@ server.tool(
4207
4207
  return {
4208
4208
  content: [{
4209
4209
  type: "text",
4210
- text: `📋 **Decision Templates**\n\n${list}\n\n---\n\n\`decision_start(problem: "...", template: "lib-compare")\`로 사용하세요.`,
4210
+ text: `📋 **Decision Templates**\n\n${list}\n\n---\n\nUse with \`decision_start(problem: "...", template: "lib-compare")\`.`,
4211
4211
  }],
4212
4212
  };
4213
4213
  })