@dmsdc-ai/aigentry-deliberation 0.0.15 → 0.0.18

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/README.md CHANGED
@@ -53,6 +53,21 @@ cd aigentry-deliberation && npm install && node install.js
53
53
  npx @dmsdc-ai/aigentry-deliberation uninstall
54
54
  ```
55
55
 
56
+ ## Forum Demo
57
+
58
+ Deliberation이 완료되면 결과를 시각화하는 Forum View를 생성합니다.
59
+
60
+ > **Deliberation = 프로세스, Forum = 출력물.**
61
+ > Deliberation이 끝나면 Forum이 생성되고, 그게 끝입니다.
62
+
63
+ ![Forum Demo](demo/forum/assets/hero.png)
64
+
65
+ 정적 데모를 브라우저에서 확인하려면:
66
+
67
+ ```bash
68
+ open demo/forum/index.html
69
+ ```
70
+
56
71
  ## MCP Tools
57
72
 
58
73
  | Tool | Description |
@@ -71,6 +86,7 @@ npx @dmsdc-ai/aigentry-deliberation uninstall
71
86
  | `deliberation_browser_auto_turn` | Auto-send turn to browser LLM |
72
87
  | `deliberation_route_turn` | Route turn to appropriate transport |
73
88
  | `deliberation_request_review` | Request code review |
89
+ | `deliberation_cli_auto_turn` | Auto-send turn to CLI speaker |
74
90
  | `deliberation_cli_config` | Configure CLI settings |
75
91
 
76
92
  ## Speaker Ordering Strategies
@@ -420,6 +420,10 @@ class DevToolsMcpAdapter extends BrowserControlPort {
420
420
  type: "rawKeyDown", key: "Enter", code: "Enter",
421
421
  windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
422
422
  });
423
+ await this._cdpCommand(binding, "Input.dispatchKeyEvent", {
424
+ type: "char", key: "\r", code: "Enter",
425
+ windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
426
+ });
423
427
  await this._cdpCommand(binding, "Input.dispatchKeyEvent", {
424
428
  type: "keyUp", key: "Enter", code: "Enter",
425
429
  windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
package/index.js CHANGED
@@ -54,6 +54,7 @@ Usage:
54
54
  * deliberation_speaker_candidates 선택 가능한 스피커 후보(로컬 CLI + 브라우저 LLM 탭) 조회
55
55
  * deliberation_browser_llm_tabs 브라우저 LLM 탭 목록 조회
56
56
  * deliberation_browser_auto_turn 브라우저 LLM에 자동으로 턴을 전송하고 응답을 수집 (CDP 기반)
57
+ * deliberation_cli_auto_turn CLI speaker에 자동으로 턴을 전송하고 응답을 수집
57
58
  * deliberation_request_review 코드 리뷰 요청 (CLI 리뷰어 자동 호출, sync/async 모드)
58
59
  */
59
60
 
@@ -280,7 +281,7 @@ const DEGRADATION_TIERS = {
280
281
  tier3: { name: "manual", description: "완전 수동 복사/붙여넣기", check: () => true },
281
282
  },
282
283
  terminal: {
283
- tier1: { name: "auto_open", description: "터미널 앱 자동 오픈", check: () => process.platform === "darwin" || process.platform === "win32" },
284
+ tier1: { name: "auto_open", description: "터미널 앱 자동 오픈", check: () => process.platform === "darwin" || process.platform === "linux" || process.platform === "win32" },
284
285
  tier2: { name: "none", description: "터미널 자동 오픈 불가", check: () => true },
285
286
  tier3: { name: "none", description: "터미널 자동 오픈 불가", check: () => true },
286
287
  },
@@ -869,37 +870,82 @@ async function ensureCdpAvailable() {
869
870
  } catch { /* not reachable */ }
870
871
  }
871
872
 
872
- // If none respond and platform is macOS, try auto-launching Chrome with CDP
873
- if (process.platform === "darwin") {
873
+ // Auto-launch Chrome with CDP on macOS, Linux, and Windows
874
+ {
875
+ let chromeBin, chromeUserDataDir;
876
+
877
+ if (process.platform === "darwin") {
878
+ chromeBin = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
879
+ chromeUserDataDir = path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome");
880
+ } else if (process.platform === "linux") {
881
+ const chromeCandidates = ["google-chrome", "google-chrome-stable", "google-chrome-beta", "chromium-browser", "chromium"];
882
+ chromeBin = chromeCandidates.find(c => commandExistsInPath(c)) || null;
883
+ if (!chromeBin) {
884
+ return {
885
+ available: false,
886
+ reason: "Chrome/Chromium을 찾을 수 없습니다. google-chrome 또는 chromium을 설치하고 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
887
+ };
888
+ }
889
+ const googleDir = path.join(os.homedir(), ".config", "google-chrome");
890
+ const chromiumDir = path.join(os.homedir(), ".config", "chromium");
891
+ chromeUserDataDir = fs.existsSync(googleDir) ? googleDir : fs.existsSync(chromiumDir) ? chromiumDir : null;
892
+ } else if (process.platform === "win32") {
893
+ const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
894
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
895
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
896
+ const winCandidates = [
897
+ path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
898
+ path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
899
+ path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
900
+ path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
901
+ path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
902
+ ];
903
+ chromeBin = winCandidates.find(p => fs.existsSync(p)) || null;
904
+ if (!chromeBin) {
905
+ return {
906
+ available: false,
907
+ reason: "Chrome/Edge를 찾을 수 없습니다. Chrome을 설치하거나 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
908
+ };
909
+ }
910
+ const chromeDir = path.join(localAppData, "Google", "Chrome", "User Data");
911
+ const edgeDir = path.join(localAppData, "Microsoft", "Edge", "User Data");
912
+ chromeUserDataDir = fs.existsSync(chromeDir) ? chromeDir : fs.existsSync(edgeDir) ? edgeDir : null;
913
+ } else {
914
+ return {
915
+ available: false,
916
+ reason: "Chrome CDP를 활성화할 수 없습니다. Chrome을 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
917
+ };
918
+ }
919
+
874
920
  // Chrome 145+ requires --user-data-dir for CDP to work.
875
921
  // The default data dir is rejected, so we copy the profile to ~/.chrome-cdp.
876
- const chromeUserDataDir = path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome");
877
922
  const cdpDataDir = path.join(os.homedir(), ".chrome-cdp");
878
923
  const profileDir = "Default";
879
924
 
880
925
  try {
881
- const srcProfile = path.join(chromeUserDataDir, profileDir);
882
- const dstProfile = path.join(cdpDataDir, profileDir);
883
- if (!fs.existsSync(dstProfile) && fs.existsSync(srcProfile)) {
884
- fs.mkdirSync(cdpDataDir, { recursive: true });
885
- execFileSync("cp", ["-R", srcProfile, dstProfile], { timeout: 30000, stdio: "ignore" });
886
- // Create minimal Local State with single profile to avoid profile picker
887
- const localStateSrc = path.join(chromeUserDataDir, "Local State");
888
- if (fs.existsSync(localStateSrc)) {
889
- const state = JSON.parse(fs.readFileSync(localStateSrc, "utf8"));
890
- state.profile.profiles_created = 1;
891
- state.profile.last_used = profileDir;
892
- if (state.profile.info_cache) {
893
- const kept = {};
894
- if (state.profile.info_cache[profileDir]) kept[profileDir] = state.profile.info_cache[profileDir];
895
- state.profile.info_cache = kept;
926
+ if (chromeUserDataDir) {
927
+ const srcProfile = path.join(chromeUserDataDir, profileDir);
928
+ const dstProfile = path.join(cdpDataDir, profileDir);
929
+ if (!fs.existsSync(dstProfile) && fs.existsSync(srcProfile)) {
930
+ fs.mkdirSync(cdpDataDir, { recursive: true });
931
+ execFileSync("cp", ["-R", srcProfile, dstProfile], { timeout: 30000, stdio: "ignore" });
932
+ // Create minimal Local State with single profile to avoid profile picker
933
+ const localStateSrc = path.join(chromeUserDataDir, "Local State");
934
+ if (fs.existsSync(localStateSrc)) {
935
+ const state = JSON.parse(fs.readFileSync(localStateSrc, "utf8"));
936
+ state.profile.profiles_created = 1;
937
+ state.profile.last_used = profileDir;
938
+ if (state.profile.info_cache) {
939
+ const kept = {};
940
+ if (state.profile.info_cache[profileDir]) kept[profileDir] = state.profile.info_cache[profileDir];
941
+ state.profile.info_cache = kept;
942
+ }
943
+ fs.writeFileSync(path.join(cdpDataDir, "Local State"), JSON.stringify(state));
896
944
  }
897
- fs.writeFileSync(path.join(cdpDataDir, "Local State"), JSON.stringify(state));
898
945
  }
899
946
  }
900
947
  } catch { /* proceed with launch attempt anyway */ }
901
948
 
902
- const chromeBin = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
903
949
  const launchArgs = [
904
950
  "--remote-debugging-port=9222",
905
951
  "--remote-allow-origins=*",
@@ -914,7 +960,7 @@ async function ensureCdpAvailable() {
914
960
  } catch {
915
961
  return {
916
962
  available: false,
917
- reason: "Chrome 자동 실행에 실패했습니다. Chrome을 수동으로 --remote-debugging-port=9222 --user-data-dir=~/.chrome-cdp 옵션과 함께 실행해주세요.",
963
+ reason: `Chrome 자동 실행에 실패했습니다. Chrome을 수동으로 --remote-debugging-port=9222 --user-data-dir=~/.chrome-cdp 옵션과 함께 실행해주세요.`,
918
964
  };
919
965
  }
920
966
 
@@ -937,7 +983,7 @@ async function ensureCdpAvailable() {
937
983
  };
938
984
  }
939
985
 
940
- // Non-macOS: cannot auto-launch
986
+ // Unreachable (all platforms handled above), but keep as safety net
941
987
  return {
942
988
  available: false,
943
989
  reason: "Chrome CDP를 활성화할 수 없습니다. Chrome을 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
@@ -2275,7 +2321,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2275
2321
  return {
2276
2322
  content: [{
2277
2323
  type: "text",
2278
- text: `✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} 완료.\n\n🏁 **모든 라운드 종료!**\ndeliberation_synthesize(session_id: "${state.id}")로 합성 보고서를 작성하세요.`,
2324
+ text: `✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} 완료. Forum 업데이트됨 (${state.log.length}건 응답 축적).\n\n🏁 **모든 라운드 종료!**\ndeliberation_synthesize(session_id: "${state.id}")로 합성 보고서를 작성하세요.`,
2279
2325
  }],
2280
2326
  };
2281
2327
  }
@@ -2290,7 +2336,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
2290
2336
  return {
2291
2337
  content: [{
2292
2338
  type: "text",
2293
- text: `✅ [${state.id}] ${normalizedSpeaker} Round ${state.log[state.log.length - 1].round} 완료.\n\n**다음:** ${state.current_speaker} (Round ${state.current_round})`,
2339
+ 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})`,
2294
2340
  }],
2295
2341
  };
2296
2342
  });
@@ -2576,7 +2622,7 @@ server.tool(
2576
2622
  return {
2577
2623
  content: [{
2578
2624
  type: "text",
2579
- text: `✅ Deliberation 시작!\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}\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}" 를 사용하세요.`,
2625
+ 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}\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}")\``,
2580
2626
  }],
2581
2627
  };
2582
2628
  })
@@ -2637,7 +2683,7 @@ server.tool(
2637
2683
  return {
2638
2684
  content: [{
2639
2685
  type: "text",
2640
- text: `**세션:** ${state.id}\n**프로젝트:** ${state.project}\n**주제:** ${state.topic}\n**상태:** ${state.status}\n**라운드:** ${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)}` : ""}`,
2686
+ 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)}` : ""}`,
2641
2687
  }],
2642
2688
  };
2643
2689
  }
@@ -2894,6 +2940,153 @@ server.tool(
2894
2940
  })
2895
2941
  );
2896
2942
 
2943
+ server.tool(
2944
+ "deliberation_cli_auto_turn",
2945
+ "CLI speaker에 자동으로 턴을 전송하고 응답을 수집합니다.",
2946
+ {
2947
+ session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
2948
+ timeout_sec: z.number().optional().default(120).describe("CLI 응답 대기 타임아웃 (초)"),
2949
+ },
2950
+ safeToolHandler("deliberation_cli_auto_turn", async ({ session_id, timeout_sec }) => {
2951
+ const resolved = resolveSessionId(session_id);
2952
+ if (!resolved) {
2953
+ return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
2954
+ }
2955
+ if (resolved === "MULTIPLE") {
2956
+ return { content: [{ type: "text", text: multipleSessionsError() }] };
2957
+ }
2958
+
2959
+ const state = loadSession(resolved);
2960
+ if (!state || state.status !== "active") {
2961
+ return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
2962
+ }
2963
+
2964
+ const speaker = state.current_speaker;
2965
+ if (speaker === "none") {
2966
+ return { content: [{ type: "text", text: "현재 발언 차례인 speaker가 없습니다." }] };
2967
+ }
2968
+
2969
+ const { transport } = resolveTransportForSpeaker(state, speaker);
2970
+ if (transport !== "cli_respond") {
2971
+ return { content: [{ type: "text", text: `speaker "${speaker}"는 CLI 타입이 아닙니다 (transport: ${transport}). 브라우저 speaker는 deliberation_browser_auto_turn을 사용하세요.` }] };
2972
+ }
2973
+
2974
+ const hint = CLI_INVOCATION_HINTS[speaker];
2975
+ if (!hint) {
2976
+ return { content: [{ type: "text", text: `speaker "${speaker}"에 대한 CLI 호출 정보가 없습니다. CLI_INVOCATION_HINTS에 등록되지 않은 speaker입니다.` }] };
2977
+ }
2978
+
2979
+ // Check CLI liveness
2980
+ if (!checkCliLiveness(hint.cmd)) {
2981
+ return { content: [{ type: "text", text: `❌ CLI "${hint.cmd}"가 설치되어 있지 않거나 실행할 수 없습니다.` }] };
2982
+ }
2983
+
2984
+ const turnId = state.pending_turn_id || generateTurnId();
2985
+ const turnPrompt = buildClipboardTurnPrompt(state, speaker, null, 3);
2986
+
2987
+ // Spawn CLI process
2988
+ const startTime = Date.now();
2989
+ try {
2990
+ const response = await new Promise((resolve, reject) => {
2991
+ const env = { ...process.env };
2992
+ // Unset CLAUDECODE for claude to avoid nested session errors
2993
+ if (hint.envPrefix?.includes("CLAUDECODE=")) {
2994
+ delete env.CLAUDECODE;
2995
+ }
2996
+
2997
+ let child;
2998
+ let stdout = "";
2999
+ let stderr = "";
3000
+
3001
+ // Different invocation patterns per CLI
3002
+ switch (speaker) {
3003
+ case "claude":
3004
+ child = spawn("claude", ["-p", "--output-format", "text"], { env, windowsHide: true });
3005
+ child.stdin.write(turnPrompt);
3006
+ child.stdin.end();
3007
+ break;
3008
+ case "codex":
3009
+ child = spawn("codex", ["exec", turnPrompt], { env, windowsHide: true });
3010
+ break;
3011
+ case "gemini":
3012
+ child = spawn("gemini", ["-p", turnPrompt], { env, windowsHide: true });
3013
+ break;
3014
+ default: {
3015
+ // Generic: try command with prompt as argument
3016
+ const flags = hint.flags ? hint.flags.split(/\s+/) : [];
3017
+ child = spawn(hint.cmd, [...flags, turnPrompt], { env, windowsHide: true });
3018
+ break;
3019
+ }
3020
+ }
3021
+
3022
+ const timer = setTimeout(() => {
3023
+ child.kill("SIGTERM");
3024
+ reject(new Error(`CLI 타임아웃 (${timeout_sec}초)`));
3025
+ }, timeout_sec * 1000);
3026
+
3027
+ child.stdout.on("data", (data) => { stdout += data.toString(); });
3028
+ child.stderr.on("data", (data) => { stderr += data.toString(); });
3029
+
3030
+ child.on("close", (code) => {
3031
+ clearTimeout(timer);
3032
+ if (code !== 0 && !stdout.trim()) {
3033
+ reject(new Error(`CLI exit code ${code}: ${stderr.slice(0, 500)}`));
3034
+ } else {
3035
+ // Clean up output noise
3036
+ let cleaned = stdout;
3037
+ if (speaker === "codex") {
3038
+ cleaned = stdout.split("\n")
3039
+ .filter(line => !/^(OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|mcp:|thinking$|tokens used$|^[0-9,]*$)/.test(line))
3040
+ .join("\n");
3041
+ } else if (speaker === "gemini") {
3042
+ cleaned = stdout.split("\n")
3043
+ .filter(line => !/^(Loaded cached|Error during discovery)/.test(line))
3044
+ .join("\n");
3045
+ }
3046
+ resolve(cleaned.trim());
3047
+ }
3048
+ });
3049
+
3050
+ child.on("error", (err) => {
3051
+ clearTimeout(timer);
3052
+ reject(err);
3053
+ });
3054
+ });
3055
+
3056
+ const elapsedMs = Date.now() - startTime;
3057
+
3058
+ if (!response) {
3059
+ return { content: [{ type: "text", text: `⚠️ CLI "${speaker}"가 빈 응답을 반환했습니다.` }] };
3060
+ }
3061
+
3062
+ // Submit the response
3063
+ const result = submitDeliberationTurn({
3064
+ session_id: resolved,
3065
+ speaker,
3066
+ content: response,
3067
+ turn_id: turnId,
3068
+ channel_used: "cli_auto",
3069
+ fallback_reason: null,
3070
+ });
3071
+
3072
+ return {
3073
+ content: [{
3074
+ type: "text",
3075
+ 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}`,
3076
+ }],
3077
+ };
3078
+
3079
+ } catch (err) {
3080
+ return {
3081
+ content: [{
3082
+ type: "text",
3083
+ text: `❌ CLI 자동 턴 실패: ${err.message}\n\n**Speaker:** ${speaker}\n**CLI:** ${hint.cmd}\n\ndeliberation_respond(speaker: "${speaker}", content: "...")로 수동 응답을 제출할 수 있습니다.`,
3084
+ }],
3085
+ };
3086
+ }
3087
+ })
3088
+ );
3089
+
2897
3090
  server.tool(
2898
3091
  "deliberation_respond",
2899
3092
  "현재 턴의 응답을 제출합니다.",
@@ -2905,6 +3098,32 @@ server.tool(
2905
3098
  turn_id: z.string().optional().describe("턴 검증 ID (deliberation_route_turn에서 받은 값)"),
2906
3099
  },
2907
3100
  safeToolHandler("deliberation_respond", async ({ session_id, speaker, content, content_file, turn_id }) => {
3101
+ // Guard: prevent orchestrator from fabricating responses for CLI/browser speakers
3102
+ const resolved = resolveSessionId(session_id);
3103
+ if (resolved && resolved !== "MULTIPLE") {
3104
+ const state = loadSession(resolved);
3105
+ if (state) {
3106
+ const { transport } = resolveTransportForSpeaker(state, speaker);
3107
+ if (transport === "cli_respond" || transport === "browser_auto") {
3108
+ // Check if caller is the same speaker (legitimate self-response) or an impersonator
3109
+ const callerIsSpeaker = (speaker === "claude"); // orchestrator can only legitimately respond as "claude"
3110
+ if (!callerIsSpeaker) {
3111
+ return {
3112
+ content: [{
3113
+ type: "text",
3114
+ text: `⚠️ **대리 응답 차단**: speaker "${speaker}"는 ${transport} transport입니다.\n\n` +
3115
+ `오케스트레이터가 다른 speaker를 대신하여 응답을 작성하는 것은 허용되지 않습니다.\n` +
3116
+ `대신 다음 도구를 사용하세요:\n` +
3117
+ `- CLI speaker → \`deliberation_route_turn\` 또는 \`deliberation_cli_auto_turn\`\n` +
3118
+ `- 브라우저 speaker → \`deliberation_route_turn\` 또는 \`deliberation_browser_auto_turn\`\n\n` +
3119
+ `이 도구들이 실제 CLI/브라우저를 실행하여 진짜 응답을 수집합니다.`,
3120
+ }],
3121
+ };
3122
+ }
3123
+ }
3124
+ }
3125
+ }
3126
+
2908
3127
  // Support reading content from file to avoid JSON escaping issues
2909
3128
  let finalContent = content;
2910
3129
  if (content_file && !content) {
@@ -3001,7 +3220,7 @@ server.tool(
3001
3220
  return {
3002
3221
  content: [{
3003
3222
  type: "text",
3004
- text: `✅ [${state.id}] Deliberation 완료!\n\n**프로젝트:** ${state.project}\n**주제:** ${state.topic}\n**라운드:** ${state.max_rounds}\n**응답:** ${state.log.length}건\n\n📁 ${archivePath}\n🖥️ 모니터 터미널이 즉시 강제 종료되었습니다.`,
3223
+ 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🖥️ 모니터 터미널이 즉시 강제 종료되었습니다.`,
3005
3224
  }],
3006
3225
  };
3007
3226
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-deliberation",
3
- "version": "0.0.15",
3
+ "version": "0.0.18",
4
4
  "description": "MCP Deliberation Server — Multi-session AI deliberation with smart speaker ordering and persona roles",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,11 +54,24 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
54
54
  - "크롬", "브라우저", "웹 LLM", "chrome", "browser LLM"
55
55
  - "{주제} 토론", "{주제} deliberation"
56
56
 
57
+ ## 절대 규칙 (MUST / NEVER)
58
+
59
+ > **이 규칙은 예외 없이 반드시 지켜야 합니다.**
60
+
61
+ 1. **MUST** — 토론 시작 전 반드시 `deliberation_speaker_candidates`로 후보 조회 후 `AskUserQuestion(multiSelect)`으로 사용자에게 참가자 선택을 받아야 합니다.
62
+ 2. **MUST** — 각 턴 진행 시 반드시 `deliberation_route_turn`을 사용해야 합니다. 이 도구가 transport를 자동 감지합니다:
63
+ - CLI speaker → `deliberation_cli_auto_turn`으로 실제 CLI 실행
64
+ - browser_auto → CDP로 자동 전송/수집
65
+ - clipboard/manual → 클립보드 준비 + 사용자 안내
66
+ 3. **NEVER** — 오케스트레이터(자기 자신)가 다른 speaker를 대신하여 `deliberation_respond`에 응답을 작성하지 마세요. 이것은 "역할극"이며 실제 deliberation이 아닙니다. MCP 서버가 이를 감지하고 차단합니다.
67
+ 4. **NEVER** — `deliberation_respond`를 직접 호출하지 마세요 (자기 자신의 응답 제외). 다른 speaker의 턴은 반드시 `deliberation_route_turn` 또는 `deliberation_cli_auto_turn`/`deliberation_browser_auto_turn`을 통해 진행합니다.
68
+ 5. **MUST** — 자기 자신(오케스트레이터 역할의 claude)이 speaker인 경우에만 직접 `deliberation_respond`로 응답을 제출할 수 있습니다.
69
+
57
70
  ## 워크플로우
58
71
 
59
72
  ### A. 사용자 선택형 진행 (권장)
60
73
  1. `deliberation_speaker_candidates` → 참가 가능한 CLI/브라우저 speaker 확인
61
- 2. **AskUserQuestion으로 참가자 선택** — 감지된 CLI/브라우저 speaker 목록을 `multiSelect: true`로 제시하여 사용자가 원하는 참가자만 체크. 예:
74
+ 2. **AskUserQuestion으로 참가자 선택 (필수)** — 감지된 CLI/브라우저 speaker 목록을 `multiSelect: true`로 제시하여 사용자가 원하는 참가자만 체크. 예:
62
75
  ```
63
76
  AskUserQuestion({
64
77
  questions: [{
@@ -77,9 +90,10 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
77
90
  })
78
91
  ```
79
92
  3. `deliberation_start` (선택된 speakers 전달) → session_id 획득
80
- 4. `deliberation_route_turn` → 현재 차례 speaker transport 자동 결정
81
- - CLI speaker → 자동 응답
93
+ 4. **`deliberation_route_turn` 호출 (필수)** → 현재 차례 speaker transport 자동 결정 및 실행
94
+ - CLI speaker → `deliberation_cli_auto_turn`이 실제 CLI를 실행하고 응답 수집
82
95
  - browser_auto → CDP로 자동 전송/수집
96
+ - 자기 자신(claude)이 speaker → 직접 `deliberation_respond`로 응답 제출
83
97
  5. 반복 후 `deliberation_synthesize(session_id)` → 합성 완료
84
98
  6. 구현이 필요하면 `deliberation-executor` 스킬로 handoff
85
99
  예: "session_id {id} 합의안 구현해줘"