@dmsdc-ai/aigentry-devkit 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.ps1 CHANGED
@@ -116,17 +116,20 @@ Write-Header "5. MCP Registration"
116
116
  $mcpConfig = Join-Path $ClaudeDir ".mcp.json"
117
117
  New-Item -ItemType Directory -Path $ClaudeDir -Force | Out-Null
118
118
 
119
+ $cfg = $null
119
120
  if (Test-Path $mcpConfig) {
120
121
  try {
121
- $cfg = Get-Content $mcpConfig -Raw | ConvertFrom-Json
122
- } catch {
123
- $cfg = [pscustomobject]@{}
124
- }
125
- } else {
122
+ $raw = Get-Content $mcpConfig -Raw
123
+ if ($raw) {
124
+ $cfg = $raw | ConvertFrom-Json
125
+ }
126
+ } catch {}
127
+ }
128
+ if (-not $cfg) {
126
129
  $cfg = [pscustomobject]@{}
127
130
  }
128
131
 
129
- if (-not $cfg.PSObject.Properties.Name.Contains("mcpServers")) {
132
+ if (-not ($cfg.PSObject.Properties.Match("mcpServers"))) {
130
133
  $cfg | Add-Member -MemberType NoteProperty -Name mcpServers -Value ([pscustomobject]@{})
131
134
  }
132
135
 
package/install.sh CHANGED
@@ -220,9 +220,65 @@ else
220
220
  warn "direnv not found. Skipping .envrc setup."
221
221
  fi
222
222
 
223
- # ── Codex MCP 등록 (가능하면) ──
224
- header "7. Codex Integration (optional)"
223
+ # ── 참가자 CLI 선택 ──
224
+ header "7. Participant CLI Selection"
225
+
226
+ # Detect available CLIs
227
+ AVAILABLE_CLIS=""
228
+ for cli in claude codex gemini qwen chatgpt aider llm opencode cursor; do
229
+ if command -v "$cli" >/dev/null 2>&1; then
230
+ AVAILABLE_CLIS="$AVAILABLE_CLIS $cli"
231
+ fi
232
+ done
233
+ AVAILABLE_CLIS=$(echo "$AVAILABLE_CLIS" | xargs) # trim
234
+
235
+ if [ -z "$AVAILABLE_CLIS" ]; then
236
+ warn "No participant CLIs detected. Install claude, codex, gemini, or other AI CLIs."
237
+ else
238
+ info "Detected CLIs: $AVAILABLE_CLIS"
239
+ echo ""
240
+
241
+ # Interactive selection (skip if --force flag or non-interactive)
242
+ DELIBERATION_CONFIG="$MCP_DEST/config.json"
243
+ if [ -t 0 ] && [ "${FORCE_INSTALL:-}" != "true" ]; then
244
+ echo -e " ${BOLD}Select CLIs to enable for deliberation:${NC}"
245
+ echo ""
246
+
247
+ SELECTED_CLIS=""
248
+ for cli in $AVAILABLE_CLIS; do
249
+ printf " Enable ${CYAN}%-12s${NC} for deliberation? [Y/n] " "$cli"
250
+ read -r answer </dev/tty
251
+ case "$answer" in
252
+ [nN]*) ;;
253
+ *) SELECTED_CLIS="$SELECTED_CLIS $cli" ;;
254
+ esac
255
+ done
256
+ SELECTED_CLIS=$(echo "$SELECTED_CLIS" | xargs)
257
+
258
+ if [ -z "$SELECTED_CLIS" ]; then
259
+ warn "No CLIs selected. All detected CLIs will be available by default."
260
+ SELECTED_CLIS="$AVAILABLE_CLIS"
261
+ fi
262
+ else
263
+ # Non-interactive: enable all detected
264
+ SELECTED_CLIS="$AVAILABLE_CLIS"
265
+ fi
225
266
 
267
+ info "Enabled CLIs: $SELECTED_CLIS"
268
+
269
+ # Save config
270
+ node -e "
271
+ const fs = require('fs');
272
+ const configPath = '$DELIBERATION_CONFIG';
273
+ let config = {};
274
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch {}
275
+ config.enabled_clis = '${SELECTED_CLIS}'.split(/\s+/).filter(Boolean);
276
+ config.updated = new Date().toISOString();
277
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
278
+ " && info "Saved CLI config to $DELIBERATION_CONFIG" || warn "Failed to save CLI config"
279
+ fi
280
+
281
+ # Codex MCP registration (only CLI with native MCP support)
226
282
  if command -v codex >/dev/null 2>&1; then
227
283
  codex mcp add deliberation -- node "$MCP_DEST/index.js" 2>/dev/null && \
228
284
  info "Registered deliberation MCP in Codex" || \
@@ -233,14 +289,13 @@ if command -v codex >/dev/null 2>&1; then
233
289
  else
234
290
  warn "Codex MCP verification failed. Run manually: codex mcp add deliberation -- node $MCP_DEST/index.js"
235
291
  fi
236
- else
237
- warn "Codex CLI not found. Skipping Codex integration."
238
292
  fi
239
293
 
240
294
  header "8. Cross-platform Notes"
241
- info "Codex is a deliberation participant CLI, not a separate MCP server."
295
+ info "Supported participant CLIs: claude, codex, gemini, qwen, chatgpt, aider, llm, opencode, cursor"
296
+ info "Manage enabled CLIs anytime: deliberation_cli_config MCP tool"
242
297
  info "Browser LLM tab detection: macOS automation + CDP scan (Linux/Windows need browser remote-debugging port)."
243
- info "If browser tab auto-scan is unavailable, use clipboard workflow: prepare_turn -> paste in browser -> submit_turn."
298
+ info "CDP auto-detect upgrades browser speakers to browser_auto for hands-free operation."
244
299
 
245
300
  # ── 완료 ──
246
301
  header "Installation Complete!"
@@ -252,9 +307,10 @@ echo -e " MCP Server: $MCP_DEST"
252
307
  echo -e " Config: $CLAUDE_DIR"
253
308
  echo ""
254
309
  echo -e " ${BOLD}Next steps:${NC}"
255
- echo -e " 1. Restart Claude/Codex processes for MCP changes to take effect"
256
- echo -e " 2. Add other MCP servers to $MCP_CONFIG as needed"
257
- echo -e " 3. Configure your HUD in settings.json if not already done"
258
- echo -e " 4. For Linux/Windows browser scan, launch browser with --remote-debugging-port=9222"
310
+ echo -e " 1. Restart CLI processes for MCP changes to take effect"
311
+ echo -e " 2. Modify enabled CLIs anytime via deliberation_cli_config MCP tool"
312
+ echo -e " 3. Add other MCP servers to $MCP_CONFIG as needed"
313
+ echo -e " 4. Configure your HUD in settings.json if not already done"
314
+ echo -e " 5. For Linux/Windows browser scan, launch browser with --remote-debugging-port=9222"
259
315
  echo ""
260
316
  echo -e " ${CYAN}Enjoy your AI development environment!${NC}"
@@ -19,9 +19,8 @@
19
19
  * deliberation_reset 세션 초기화 (session_id 선택적, 없으면 전체)
20
20
  * deliberation_speaker_candidates 선택 가능한 스피커 후보(로컬 CLI + 브라우저 LLM 탭) 조회
21
21
  * deliberation_browser_llm_tabs 브라우저 LLM 탭 목록 조회
22
- * deliberation_clipboard_prepare_turn 브라우저 LLM용 턴 프롬프트를 클립보드로 복사
23
- * deliberation_clipboard_submit_turn 클립보드 텍스트를 현재 턴 응답으로 제출
24
22
  * deliberation_browser_auto_turn 브라우저 LLM에 자동으로 턴을 전송하고 응답을 수집 (CDP 기반)
23
+ * deliberation_request_review 코드 리뷰 요청 (CLI 리뷰어 자동 호출, sync/async 모드)
25
24
  */
26
25
 
27
26
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -55,6 +54,22 @@ const DEFAULT_CLI_CANDIDATES = [
55
54
  "continue",
56
55
  ];
57
56
  const MAX_AUTO_DISCOVERED_SPEAKERS = 12;
57
+
58
+ function loadDeliberationConfig() {
59
+ const configPath = path.join(HOME, ".local", "lib", "mcp-deliberation", "config.json");
60
+ try {
61
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
62
+ } catch {
63
+ return {};
64
+ }
65
+ }
66
+
67
+ function saveDeliberationConfig(config) {
68
+ const configPath = path.join(HOME, ".local", "lib", "mcp-deliberation", "config.json");
69
+ config.updated = new Date().toISOString();
70
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
71
+ }
72
+
58
73
  const DEFAULT_BROWSER_APPS = ["Google Chrome", "Brave Browser", "Arc", "Microsoft Edge", "Safari"];
59
74
  const DEFAULT_LLM_DOMAINS = [
60
75
  "chatgpt.com",
@@ -241,6 +256,13 @@ function resolveCliCandidates() {
241
256
  .split(/[,\s]+/)
242
257
  .map(v => v.trim())
243
258
  .filter(Boolean);
259
+
260
+ // If config has enabled_clis, use that as the primary filter
261
+ const config = loadDeliberationConfig();
262
+ if (Array.isArray(config.enabled_clis) && config.enabled_clis.length > 0) {
263
+ return dedupeSpeakers([...fromEnv, ...config.enabled_clis]);
264
+ }
265
+
244
266
  return dedupeSpeakers([...fromEnv, ...DEFAULT_CLI_CANDIDATES]);
245
267
  }
246
268
 
@@ -578,6 +600,59 @@ async function collectBrowserLlmTabsViaCdp() {
578
600
  };
579
601
  }
580
602
 
603
+ async function ensureCdpAvailable() {
604
+ const endpoints = resolveCdpEndpoints();
605
+
606
+ // First attempt: try existing CDP endpoints
607
+ for (const endpoint of endpoints) {
608
+ try {
609
+ const payload = await fetchJson(endpoint, 1500);
610
+ if (Array.isArray(payload)) {
611
+ return { available: true, endpoint };
612
+ }
613
+ } catch { /* not reachable */ }
614
+ }
615
+
616
+ // If none respond and platform is macOS, try auto-launching Chrome with CDP
617
+ if (process.platform === "darwin") {
618
+ try {
619
+ execFileSync("open", ["-a", "Google Chrome", "--args", "--remote-debugging-port=9222"], {
620
+ timeout: 5000,
621
+ stdio: "ignore",
622
+ });
623
+ } catch {
624
+ return {
625
+ available: false,
626
+ reason: "Chrome 자동 실행에 실패했습니다. Chrome을 수동으로 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
627
+ };
628
+ }
629
+
630
+ // Wait for Chrome to initialize CDP
631
+ sleepMs(2000);
632
+
633
+ // Retry CDP connection after launch
634
+ for (const endpoint of endpoints) {
635
+ try {
636
+ const payload = await fetchJson(endpoint, 2000);
637
+ if (Array.isArray(payload)) {
638
+ return { available: true, endpoint, launched: true };
639
+ }
640
+ } catch { /* still not reachable */ }
641
+ }
642
+
643
+ return {
644
+ available: false,
645
+ reason: "Chrome을 실행했지만 CDP에 연결할 수 없습니다. Chrome을 완전히 종료한 후 다시 시도해주세요. (이미 실행 중인 Chrome이 CDP 없이 시작된 경우 재시작 필요)",
646
+ };
647
+ }
648
+
649
+ // Non-macOS: cannot auto-launch
650
+ return {
651
+ available: false,
652
+ reason: "Chrome CDP를 활성화할 수 없습니다. Chrome을 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
653
+ };
654
+ }
655
+
581
656
  function collectBrowserLlmTabsViaAppleScript() {
582
657
  if (process.platform !== "darwin") {
583
658
  return { tabs: [], note: "AppleScript 탭 스캔은 macOS에서만 지원됩니다." };
@@ -588,56 +663,67 @@ function collectBrowserLlmTabsViaAppleScript() {
588
663
  const domainList = `{${escapedDomains.map(d => `"${d}"`).join(", ")}}`;
589
664
  const appList = `{${escapedApps.map(a => `"${a}"`).join(", ")}}`;
590
665
 
591
- const script = [
592
- `set llmDomains to ${domainList}`,
593
- `set browserApps to ${appList}`,
594
- "set outText to \"\"",
595
- // Pre-check running apps via System Events (no locate dialog)
596
- "tell application \"System Events\"",
597
- "set runningApps to name of every application process",
598
- "end tell",
599
- "repeat with appName in browserApps",
600
- "if runningApps contains (appName as string) then",
601
- "try",
602
- "tell application (appName as string)",
603
- "if (appName as string) is \"Safari\" then",
604
- "repeat with w in windows",
605
- "try",
606
- "repeat with t in tabs of w",
607
- "set u to URL of t as string",
608
- "set matched to false",
609
- "repeat with d in llmDomains",
610
- "if u contains (d as string) then set matched to true",
611
- "end repeat",
612
- "if matched then set outText to outText & (appName as string) & tab & (name of t as string) & tab & u & linefeed",
613
- "end repeat",
614
- "end try",
615
- "end repeat",
616
- "else",
617
- "repeat with w in windows",
618
- "try",
619
- "repeat with t in tabs of w",
620
- "set u to URL of t as string",
621
- "set matched to false",
622
- "repeat with d in llmDomains",
623
- "if u contains (d as string) then set matched to true",
624
- "end repeat",
625
- "if matched then set outText to outText & (appName as string) & tab & (title of t as string) & tab & u & linefeed",
626
- "end repeat",
627
- "end try",
628
- "end repeat",
629
- "end if",
630
- "end tell",
631
- "on error errMsg",
632
- "set outText to outText & (appName as string) & tab & \"ERROR\" & tab & errMsg & linefeed",
633
- "end try",
634
- "end if",
635
- "end repeat",
636
- "return outText",
637
- ];
666
+ // NOTE: Use stdin pipe (`osascript -`) instead of multiple `-e` flags
667
+ // because osascript's `-e` mode silently breaks with nested try/on error blocks.
668
+ // Also wrap dynamic `tell application` with `using terms from` so that
669
+ // Chrome-specific properties like `tabs` resolve via the scripting dictionary.
670
+ // Use ASCII character 9 for tab delimiter because `using terms from`
671
+ // shadows the built-in `tab` constant, turning it into the literal string "tab".
672
+ const scriptText = `set llmDomains to ${domainList}
673
+ set browserApps to ${appList}
674
+ set outText to ""
675
+ set tabChar to ASCII character 9
676
+ tell application "System Events"
677
+ set runningApps to name of every application process
678
+ end tell
679
+ repeat with appName in browserApps
680
+ if runningApps contains (appName as string) then
681
+ try
682
+ if (appName as string) is "Safari" then
683
+ using terms from application "Safari"
684
+ tell application (appName as string)
685
+ repeat with w in windows
686
+ try
687
+ repeat with t in tabs of w
688
+ set u to URL of t as string
689
+ set matched to false
690
+ repeat with d in llmDomains
691
+ if u contains (d as string) then set matched to true
692
+ end repeat
693
+ if matched then set outText to outText & (appName as string) & tabChar & (name of t as string) & tabChar & u & linefeed
694
+ end repeat
695
+ end try
696
+ end repeat
697
+ end tell
698
+ end using terms from
699
+ else
700
+ using terms from application "Google Chrome"
701
+ tell application (appName as string)
702
+ repeat with w in windows
703
+ try
704
+ repeat with t in tabs of w
705
+ set u to URL of t as string
706
+ set matched to false
707
+ repeat with d in llmDomains
708
+ if u contains (d as string) then set matched to true
709
+ end repeat
710
+ if matched then set outText to outText & (appName as string) & tabChar & (title of t as string) & tabChar & u & linefeed
711
+ end repeat
712
+ end try
713
+ end repeat
714
+ end tell
715
+ end using terms from
716
+ end if
717
+ on error errMsg
718
+ set outText to outText & (appName as string) & tabChar & "ERROR" & tabChar & errMsg & linefeed
719
+ end try
720
+ end if
721
+ end repeat
722
+ return outText`;
638
723
 
639
724
  try {
640
- const raw = execFileSync("osascript", script.flatMap(line => ["-e", line]), {
725
+ const raw = execFileSync("osascript", ["-"], {
726
+ input: scriptText,
641
727
  encoding: "utf-8",
642
728
  timeout: 8000,
643
729
  maxBuffer: 2 * 1024 * 1024,
@@ -745,8 +831,14 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
745
831
 
746
832
  let browserNote = null;
747
833
  if (include_browser) {
834
+ // Ensure CDP is available before probing browser tabs
835
+ const cdpStatus = await ensureCdpAvailable();
836
+ if (cdpStatus.launched) {
837
+ browserNote = "Chrome CDP 자동 실행됨 (--remote-debugging-port=9222)";
838
+ }
839
+
748
840
  const { tabs, note } = await collectBrowserLlmTabs();
749
- browserNote = note || null;
841
+ browserNote = browserNote ? `${browserNote} | ${note || ""}`.replace(/ \| $/, "") : (note || null);
750
842
  const providerCounts = new Map();
751
843
  for (const tab of tabs) {
752
844
  const provider = inferLlmProvider(tab.url);
@@ -924,7 +1016,7 @@ function formatTransportGuidance(transport, state, speaker) {
924
1016
  case "cli_respond":
925
1017
  return `CLI speaker입니다. \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`로 직접 응답하세요.`;
926
1018
  case "clipboard":
927
- return `브라우저 LLM speaker입니다. 다음 순서로 진행하세요:\n1. \`deliberation_clipboard_prepare_turn(session_id: "${sid}")\` 클립보드에 프롬프트 복사\n2. 브라우저 LLM에 붙여넣고 응답 생성\n3. 응답을 복사한 뒤 \`deliberation_clipboard_submit_turn(session_id: "${sid}", speaker: "${speaker}")\` 호출`;
1019
+ return `브라우저 LLM speaker입니다. CDP 자동 연결 시도 중... Chrome이 이미 CDP 없이 실행 중이면 재시작이 필요할 있습니다.`;
928
1020
  case "browser_auto":
929
1021
  return `자동 브라우저 speaker입니다. \`deliberation_browser_auto_turn(session_id: "${sid}")\`으로 자동 진행됩니다. CDP를 통해 브라우저 LLM에 직접 입력하고 응답을 읽습니다.`;
930
1022
  case "manual":
@@ -1803,6 +1895,23 @@ server.tool(
1803
1895
  created: new Date().toISOString(),
1804
1896
  updated: new Date().toISOString(),
1805
1897
  };
1898
+
1899
+ // Ensure CDP is ready if any speaker requires browser transport
1900
+ const hasBrowserSpeaker = state.participant_profiles.some(
1901
+ p => p.type === "browser" || p.type === "browser_auto"
1902
+ );
1903
+ if (hasBrowserSpeaker) {
1904
+ const cdpReady = await ensureCdpAvailable();
1905
+ if (!cdpReady.available) {
1906
+ return {
1907
+ content: [{
1908
+ type: "text",
1909
+ text: `❌ 브라우저 LLM speaker가 포함되어 있지만 CDP에 연결할 수 없습니다.\n\n${cdpReady.reason}\n\nCDP 연결 후 다시 deliberation_start를 호출하세요.`,
1910
+ }],
1911
+ };
1912
+ }
1913
+ }
1914
+
1806
1915
  withSessionLock(sessionId, () => {
1807
1916
  saveSession(state);
1808
1917
  });
@@ -1945,88 +2054,6 @@ server.tool(
1945
2054
  }
1946
2055
  );
1947
2056
 
1948
- server.tool(
1949
- "deliberation_clipboard_prepare_turn",
1950
- "현재 턴 요청 프롬프트를 생성해 클립보드에 복사합니다. 브라우저 LLM에 붙여넣어 사용하세요.",
1951
- {
1952
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
1953
- speaker: z.string().trim().min(1).max(64).optional().describe("대상 speaker (미지정 시 현재 차례)"),
1954
- prompt: z.string().optional().describe("브라우저 LLM에 추가로 전달할 지시"),
1955
- include_history_entries: z.number().int().min(0).max(12).default(4).describe("프롬프트에 포함할 최근 로그 개수"),
1956
- },
1957
- async ({ session_id, speaker, prompt, include_history_entries }) => {
1958
- const resolved = resolveSessionId(session_id);
1959
- if (!resolved) {
1960
- return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
1961
- }
1962
- if (resolved === "MULTIPLE") {
1963
- return { content: [{ type: "text", text: multipleSessionsError() }] };
1964
- }
1965
-
1966
- const state = loadSession(resolved);
1967
- if (!state || state.status !== "active") {
1968
- return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
1969
- }
1970
-
1971
- const targetSpeaker = normalizeSpeaker(speaker) || normalizeSpeaker(state.current_speaker) || state.speakers[0];
1972
- if (targetSpeaker !== state.current_speaker) {
1973
- return {
1974
- content: [{
1975
- type: "text",
1976
- text: `[${state.id}] 지금은 **${state.current_speaker}** 차례입니다. prepare 대상 speaker는 현재 차례와 같아야 합니다.`,
1977
- }],
1978
- };
1979
- }
1980
-
1981
- const payload = buildClipboardTurnPrompt(state, targetSpeaker, prompt, include_history_entries);
1982
- try {
1983
- writeClipboardText(payload);
1984
- } catch (error) {
1985
- const message = error instanceof Error ? error.message : "unknown error";
1986
- return { content: [{ type: "text", text: `클립보드 복사 실패: ${message}` }] };
1987
- }
1988
-
1989
- return {
1990
- content: [{
1991
- type: "text",
1992
- text: `✅ [${state.id}] 턴 프롬프트를 클립보드에 복사했습니다.\n\n**대상 speaker:** ${targetSpeaker}\n**라운드:** ${state.current_round}/${state.max_rounds}\n\n다음 단계:\n1. 브라우저 LLM에 붙여넣고 응답 생성\n2. 응답 본문을 복사\n3. deliberation_clipboard_submit_turn(session_id: "${state.id}", speaker: "${targetSpeaker}") 호출\n\n${PRODUCT_DISCLAIMER}`,
1993
- }],
1994
- };
1995
- }
1996
- );
1997
-
1998
- server.tool(
1999
- "deliberation_clipboard_submit_turn",
2000
- "클립보드 텍스트(또는 content)를 현재 턴 응답으로 제출합니다.",
2001
- {
2002
- session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
2003
- speaker: z.string().trim().min(1).max(64).describe("응답자 이름"),
2004
- content: z.string().optional().describe("응답 내용 (미지정 시 클립보드 텍스트 사용)"),
2005
- trim_content: z.boolean().default(false).describe("응답 앞뒤 공백 제거 여부"),
2006
- turn_id: z.string().optional().describe("턴 검증 ID"),
2007
- },
2008
- safeToolHandler("deliberation_clipboard_submit_turn", async ({ session_id, speaker, content, trim_content, turn_id }) => {
2009
- let body = content;
2010
- if (typeof body !== "string") {
2011
- try {
2012
- body = readClipboardText();
2013
- } catch (error) {
2014
- const message = error instanceof Error ? error.message : "unknown error";
2015
- return { content: [{ type: "text", text: `클립보드 읽기 실패: ${message}` }] };
2016
- }
2017
- }
2018
-
2019
- if (trim_content) {
2020
- body = body.trim();
2021
- }
2022
- if (!body || body.trim().length === 0) {
2023
- return { content: [{ type: "text", text: "제출할 응답이 비어 있습니다. 클립보드 또는 content를 확인하세요." }] };
2024
- }
2025
-
2026
- return submitDeliberationTurn({ session_id, speaker, content: body, turn_id, channel_used: "clipboard" });
2027
- })
2028
- );
2029
-
2030
2057
  server.tool(
2031
2058
  "deliberation_route_turn",
2032
2059
  "현재 턴의 speaker에 맞는 transport를 자동 결정하고 안내합니다. CLI speaker는 자동 응답 경로, 브라우저 speaker는 클립보드 경로로 라우팅합니다.",
@@ -2057,18 +2084,6 @@ server.tool(
2057
2084
 
2058
2085
  let extra = "";
2059
2086
 
2060
- if (transport === "clipboard" && auto_prepare_clipboard) {
2061
- // 자동으로 클립보드 prepare 실행
2062
- const payload = buildClipboardTurnPrompt(state, speaker, prompt, include_history_entries);
2063
- try {
2064
- writeClipboardText(payload);
2065
- extra = `\n\n✅ 클립보드에 턴 프롬프트가 자동 복사되었습니다.`;
2066
- } catch (error) {
2067
- const message = error instanceof Error ? error.message : "unknown error";
2068
- extra = `\n\n⚠️ 클립보드 자동 복사 실패: ${message}\n수동으로 deliberation_clipboard_prepare_turn을 호출하세요.`;
2069
- }
2070
- }
2071
-
2072
2087
  if (transport === "browser_auto") {
2073
2088
  // Auto-execute browser_auto_turn
2074
2089
  try {
@@ -2109,15 +2124,8 @@ server.tool(
2109
2124
  throw new Error(waitResult.error?.message || "no response received");
2110
2125
  }
2111
2126
  } catch (autoErr) {
2112
- // Fallback to clipboard
2113
2127
  const errMsg = autoErr instanceof Error ? autoErr.message : String(autoErr);
2114
- const payload = buildClipboardTurnPrompt(state, speaker, prompt, include_history_entries);
2115
- try {
2116
- writeClipboardText(payload);
2117
- extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). 클립보드 모드로 폴백했습니다.\n✅ 클립보드에 턴 프롬프트가 복사되었습니다.`;
2118
- } catch (clipErr) {
2119
- extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). 클립보드 복사도 실패했습니다.\n수동으로 deliberation_clipboard_prepare_turn을 호출하세요.`;
2120
- }
2128
+ extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). Chrome을 --remote-debugging-port=9222로 재시작하세요.`;
2121
2129
  }
2122
2130
  }
2123
2131
 
@@ -2195,7 +2203,7 @@ server.tool(
2195
2203
  // Step 4: Wait for response
2196
2204
  const waitResult = await port.waitTurnResult(resolved, turnId, timeout_sec);
2197
2205
  if (!waitResult.ok) {
2198
- return { content: [{ type: "text", text: `⏱️ 브라우저 LLM 응답 대기 타임아웃 (${timeout_sec}초)\n\n**에러:** ${waitResult.error.message}\n\nclipboard fallback으로 수동 진행하세요:\n1. \`deliberation_clipboard_prepare_turn(session_id: "${resolved}")\`\n2. 브라우저에 붙여넣기\n3. \`deliberation_clipboard_submit_turn(session_id: "${resolved}")\`\n\n${PRODUCT_DISCLAIMER}` }] };
2206
+ return { content: [{ type: "text", text: `⏱️ 브라우저 LLM 응답 대기 타임아웃 (${timeout_sec}초)\n\n**에러:** ${waitResult.error.message}\n\n자동 실행이 타임아웃되었습니다. Chrome이 --remote-debugging-port=9222로 실행 중인지 확인하세요.\n\n${PRODUCT_DISCLAIMER}` }] };
2199
2207
  }
2200
2208
 
2201
2209
  // Step 5: Submit the response
@@ -2436,6 +2444,333 @@ server.tool(
2436
2444
  })
2437
2445
  );
2438
2446
 
2447
+ server.tool(
2448
+ "deliberation_cli_config",
2449
+ "딜리버레이션 참가자 CLI 설정을 조회하거나 변경합니다. enabled_clis를 지정하면 저장합니다.",
2450
+ {
2451
+ enabled_clis: z.array(z.string()).optional().describe("활성화할 CLI 목록 (예: [\"claude\", \"codex\", \"gemini\"]). 미지정 시 현재 설정 조회"),
2452
+ },
2453
+ safeToolHandler("deliberation_cli_config", async ({ enabled_clis }) => {
2454
+ const config = loadDeliberationConfig();
2455
+
2456
+ if (!enabled_clis) {
2457
+ // Read mode: show current config + detected CLIs
2458
+ const detected = discoverLocalCliSpeakers();
2459
+ const configured = Array.isArray(config.enabled_clis) ? config.enabled_clis : [];
2460
+ const mode = configured.length > 0 ? "config" : "auto-detect";
2461
+
2462
+ return {
2463
+ content: [{
2464
+ type: "text",
2465
+ text: `## Deliberation CLI 설정\n\n**모드:** ${mode}\n**설정된 CLI:** ${configured.length > 0 ? configured.join(", ") : "(없음 — 전체 자동 감지)"}\n**현재 감지된 CLI:** ${detected.join(", ") || "(없음)"}\n**지원 CLI 전체:** ${DEFAULT_CLI_CANDIDATES.join(", ")}\n\n변경하려면:\n\`deliberation_cli_config(enabled_clis: ["claude", "codex"])\`\n\n전체 자동 감지로 되돌리려면:\n\`deliberation_cli_config(enabled_clis: [])\``,
2466
+ }],
2467
+ };
2468
+ }
2469
+
2470
+ // Write mode: save new config
2471
+ if (enabled_clis.length === 0) {
2472
+ // Empty array = reset to auto-detect all
2473
+ delete config.enabled_clis;
2474
+ saveDeliberationConfig(config);
2475
+ return {
2476
+ content: [{
2477
+ type: "text",
2478
+ text: `✅ CLI 설정 초기화 완료. 전체 자동 감지 모드로 전환되었습니다.\n감지 대상: ${DEFAULT_CLI_CANDIDATES.join(", ")}`,
2479
+ }],
2480
+ };
2481
+ }
2482
+
2483
+ // Validate CLIs
2484
+ const valid = [];
2485
+ const invalid = [];
2486
+ for (const cli of enabled_clis) {
2487
+ const normalized = cli.trim().toLowerCase();
2488
+ if (normalized) valid.push(normalized);
2489
+ }
2490
+
2491
+ config.enabled_clis = valid;
2492
+ saveDeliberationConfig(config);
2493
+
2494
+ // Check which are actually installed
2495
+ const installed = valid.filter(cli => {
2496
+ try {
2497
+ execFileSync(process.platform === "win32" ? "where" : "which", [cli], { stdio: "ignore" });
2498
+ return true;
2499
+ } catch { return false; }
2500
+ });
2501
+ const notInstalled = valid.filter(cli => !installed.includes(cli));
2502
+
2503
+ let result = `✅ CLI 설정 저장 완료!\n\n**활성화된 CLI:** ${valid.join(", ")}`;
2504
+ if (installed.length > 0) result += `\n**설치 확인됨:** ${installed.join(", ")}`;
2505
+ if (notInstalled.length > 0) result += `\n**⚠️ 미설치:** ${notInstalled.join(", ")} (PATH에서 찾을 수 없음)`;
2506
+
2507
+ return { content: [{ type: "text", text: result }] };
2508
+ })
2509
+ );
2510
+
2511
+ // ── Request Review (auto-review) ───────────────────────────────
2512
+
2513
+ function invokeCliReviewer(command, prompt, timeoutMs) {
2514
+ const args = ["-p", prompt, "--no-input"];
2515
+ try {
2516
+ const result = execFileSync(command, args, {
2517
+ encoding: "utf-8",
2518
+ timeout: timeoutMs,
2519
+ stdio: ["ignore", "pipe", "pipe"],
2520
+ maxBuffer: 5 * 1024 * 1024,
2521
+ windowsHide: true,
2522
+ });
2523
+ return { ok: true, response: result.trim() };
2524
+ } catch (error) {
2525
+ if (error && error.killed) {
2526
+ return { ok: false, error: "timeout" };
2527
+ }
2528
+ const msg = error instanceof Error ? error.message : String(error);
2529
+ return { ok: false, error: msg };
2530
+ }
2531
+ }
2532
+
2533
+ function buildReviewPrompt(context, question, priorReviews) {
2534
+ let prompt = `You are a code reviewer. Provide a concise, structured review.\n\n`;
2535
+ prompt += `## Context\n${context}\n\n`;
2536
+ prompt += `## Review Question\n${question}\n\n`;
2537
+ if (priorReviews.length > 0) {
2538
+ prompt += `## Prior Reviews\n`;
2539
+ for (const r of priorReviews) {
2540
+ prompt += `### ${r.reviewer}\n${r.response}\n\n`;
2541
+ }
2542
+ }
2543
+ prompt += `Respond with your review. Be specific about issues, risks, and suggestions.`;
2544
+ return prompt;
2545
+ }
2546
+
2547
+ function synthesizeReviews(context, question, reviews) {
2548
+ if (reviews.length === 0) return "(No reviews completed)";
2549
+
2550
+ let synthesis = `## Review Synthesis\n\n`;
2551
+ synthesis += `**Question:** ${question}\n`;
2552
+ synthesis += `**Reviews:** ${reviews.length}\n\n`;
2553
+
2554
+ synthesis += `### Individual Reviews\n\n`;
2555
+ for (const r of reviews) {
2556
+ synthesis += `#### ${r.reviewer}\n${r.response}\n\n`;
2557
+ }
2558
+
2559
+ if (reviews.length > 1) {
2560
+ synthesis += `### Summary\n`;
2561
+ synthesis += `${reviews.length} reviewer(s) provided feedback on: ${question}\n`;
2562
+ synthesis += `Reviewers: ${reviews.map(r => r.reviewer).join(", ")}\n`;
2563
+ }
2564
+
2565
+ return synthesis;
2566
+ }
2567
+
2568
+ server.tool(
2569
+ "deliberation_request_review",
2570
+ "코드 리뷰를 요청합니다. 여러 CLI 리뷰어에게 동시에 리뷰를 요청하고 결과를 종합합니다.",
2571
+ {
2572
+ context: z.string().describe("리뷰할 변경사항 설명 (코드, diff, 설계 등)"),
2573
+ question: z.string().describe("리뷰 질문 (예: 'Is this error handling sufficient?')"),
2574
+ reviewers: z.array(z.string().trim().min(1).max(64)).min(1).describe("리뷰어 CLI 목록 (예: [\"claude\", \"codex\"])"),
2575
+ mode: z.enum(["sync", "async"]).default("sync").describe("sync: 결과 대기 후 반환, async: session_id 즉시 반환"),
2576
+ deadline_ms: z.number().int().min(5000).max(600000).default(60000).describe("전체 타임아웃 (밀리초, 기본 60초)"),
2577
+ min_reviews: z.number().int().min(1).default(1).describe("최소 필요 리뷰 수 (기본 1)"),
2578
+ on_timeout: z.enum(["partial", "fail"]).default("partial").describe("타임아웃 시 동작: partial=부분 결과 반환, fail=에러"),
2579
+ },
2580
+ safeToolHandler("deliberation_request_review", async ({ context, question, reviewers, mode, deadline_ms, min_reviews, on_timeout }) => {
2581
+ // Validate reviewers exist in PATH
2582
+ const validReviewers = [];
2583
+ const invalidReviewers = [];
2584
+ for (const r of reviewers) {
2585
+ const normalized = normalizeSpeaker(r);
2586
+ if (!normalized) continue;
2587
+ if (commandExistsInPath(normalized)) {
2588
+ validReviewers.push(normalized);
2589
+ } else {
2590
+ invalidReviewers.push(normalized);
2591
+ }
2592
+ }
2593
+
2594
+ if (validReviewers.length === 0) {
2595
+ return {
2596
+ content: [{
2597
+ type: "text",
2598
+ text: `❌ 유효한 리뷰어가 없습니다. PATH에서 찾을 수 없는 CLI: ${invalidReviewers.join(", ")}\n\n사용 가능한 CLI를 확인하려면 deliberation_speaker_candidates를 호출하세요.`,
2599
+ }],
2600
+ };
2601
+ }
2602
+
2603
+ // Create mini-session
2604
+ const sessionId = generateSessionId("review");
2605
+ const callerSpeaker = detectCallerSpeaker() || "requester";
2606
+ const now = new Date().toISOString();
2607
+
2608
+ const state = {
2609
+ id: sessionId,
2610
+ project: getProjectSlug(),
2611
+ topic: question.slice(0, 80),
2612
+ type: "auto_review",
2613
+ status: "active",
2614
+ max_rounds: 1,
2615
+ current_round: 1,
2616
+ current_speaker: validReviewers[0],
2617
+ speakers: validReviewers,
2618
+ participant_profiles: validReviewers.map(r => ({ speaker: r, type: "cli", command: r })),
2619
+ log: [],
2620
+ synthesis: null,
2621
+ requester: callerSpeaker,
2622
+ review_context: context,
2623
+ review_question: question,
2624
+ review_mode: mode,
2625
+ review_deadline_ms: deadline_ms,
2626
+ review_min_reviews: min_reviews,
2627
+ review_on_timeout: on_timeout,
2628
+ pending_turn_id: generateTurnId(),
2629
+ monitor_terminal_window_ids: [],
2630
+ created: now,
2631
+ updated: now,
2632
+ };
2633
+
2634
+ withSessionLock(sessionId, () => {
2635
+ ensureDirs();
2636
+ saveSession(state);
2637
+ });
2638
+
2639
+ // Async mode: return immediately
2640
+ if (mode === "async") {
2641
+ const warn = invalidReviewers.length > 0
2642
+ ? `\n⚠️ PATH에서 찾을 수 없는 리뷰어 (제외됨): ${invalidReviewers.join(", ")}`
2643
+ : "";
2644
+ return {
2645
+ content: [{
2646
+ type: "text",
2647
+ text: `✅ 비동기 리뷰 세션 생성됨\n\n**Session ID:** ${sessionId}\n**리뷰어:** ${validReviewers.join(", ")}\n**모드:** async${warn}\n\n진행 상태는 \`deliberation_status(session_id: "${sessionId}")\`로 확인하세요.`,
2648
+ }],
2649
+ };
2650
+ }
2651
+
2652
+ // Sync mode: invoke each reviewer sequentially with deadline enforcement
2653
+ const globalStart = Date.now();
2654
+ const softBudgetPerReviewer = Math.floor(deadline_ms / validReviewers.length);
2655
+ const completedReviews = [];
2656
+ const timedOutReviewers = [];
2657
+ const failedReviewers = [];
2658
+
2659
+ for (const reviewer of validReviewers) {
2660
+ const elapsed = Date.now() - globalStart;
2661
+ const remaining = deadline_ms - elapsed;
2662
+
2663
+ // Global deadline check
2664
+ if (remaining <= 1000) {
2665
+ timedOutReviewers.push(reviewer);
2666
+ continue;
2667
+ }
2668
+
2669
+ // Per-reviewer timeout: min of soft budget and remaining global time
2670
+ const reviewerTimeout = Math.min(softBudgetPerReviewer, remaining);
2671
+
2672
+ const prompt = buildReviewPrompt(context, question, completedReviews);
2673
+ const result = invokeCliReviewer(reviewer, prompt, reviewerTimeout);
2674
+
2675
+ if (result.ok) {
2676
+ const entry = { reviewer, response: result.response };
2677
+ completedReviews.push(entry);
2678
+
2679
+ // Add to session log
2680
+ withSessionLock(sessionId, () => {
2681
+ const latest = loadSession(sessionId);
2682
+ if (!latest) return;
2683
+ latest.log.push({
2684
+ round: 1,
2685
+ speaker: reviewer,
2686
+ content: result.response,
2687
+ timestamp: new Date().toISOString(),
2688
+ turn_id: generateTurnId(),
2689
+ channel_used: "cli_auto_review",
2690
+ fallback_reason: null,
2691
+ });
2692
+ latest.updated = new Date().toISOString();
2693
+ saveSession(latest);
2694
+ });
2695
+ } else if (result.error === "timeout") {
2696
+ timedOutReviewers.push(reviewer);
2697
+ } else {
2698
+ failedReviewers.push({ reviewer, error: result.error });
2699
+ }
2700
+ }
2701
+
2702
+ // Check min_reviews threshold
2703
+ if (completedReviews.length < min_reviews) {
2704
+ if (on_timeout === "fail") {
2705
+ // Mark session as failed
2706
+ withSessionLock(sessionId, () => {
2707
+ const latest = loadSession(sessionId);
2708
+ if (!latest) return;
2709
+ latest.status = "completed";
2710
+ latest.synthesis = `Review failed: only ${completedReviews.length}/${min_reviews} required reviews completed.`;
2711
+ saveSession(latest);
2712
+ archiveState(latest);
2713
+ cleanupSyncMarkdown(latest);
2714
+ });
2715
+
2716
+ return {
2717
+ content: [{
2718
+ type: "text",
2719
+ 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(", ") || "(없음)"}`,
2720
+ }],
2721
+ };
2722
+ }
2723
+ // on_timeout === "partial": fall through to return partial results
2724
+ }
2725
+
2726
+ // Synthesize
2727
+ const synthesis = synthesizeReviews(context, question, completedReviews);
2728
+
2729
+ // Complete session
2730
+ let archivePath = null;
2731
+ withSessionLock(sessionId, () => {
2732
+ const latest = loadSession(sessionId);
2733
+ if (!latest) return;
2734
+ latest.status = "completed";
2735
+ latest.synthesis = synthesis;
2736
+ latest.current_speaker = "none";
2737
+ saveSession(latest);
2738
+ archivePath = archiveState(latest);
2739
+ cleanupSyncMarkdown(latest);
2740
+ });
2741
+
2742
+ const totalMs = Date.now() - globalStart;
2743
+ const coverage = `${completedReviews.length}/${validReviewers.length}`;
2744
+ const warn = invalidReviewers.length > 0
2745
+ ? `\n**제외된 리뷰어 (미설치):** ${invalidReviewers.join(", ")}`
2746
+ : "";
2747
+ const timeoutInfo = timedOutReviewers.length > 0
2748
+ ? `\n**타임아웃 리뷰어:** ${timedOutReviewers.join(", ")}`
2749
+ : "";
2750
+ const failInfo = failedReviewers.length > 0
2751
+ ? `\n**실패 리뷰어:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ")}`
2752
+ : "";
2753
+
2754
+ const resultPayload = {
2755
+ synthesis,
2756
+ completed_reviewers: completedReviews.map(r => r.reviewer),
2757
+ timed_out_reviewers: timedOutReviewers,
2758
+ failed_reviewers: failedReviewers.map(r => r.reviewer),
2759
+ coverage,
2760
+ mode: "sync",
2761
+ session_id: sessionId,
2762
+ elapsed_ms: totalMs,
2763
+ };
2764
+
2765
+ return {
2766
+ content: [{
2767
+ type: "text",
2768
+ 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\`\`\``,
2769
+ }],
2770
+ };
2771
+ })
2772
+ );
2773
+
2439
2774
  // ── Start ──────────────────────────────────────────────────────
2440
2775
 
2441
2776
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-devkit",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Cross-platform installer and tooling bundle for aigentry-devkit",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,9 +29,7 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
29
29
  | `deliberation_status` | 토론 상태 조회 | 선택적* |
30
30
  | `deliberation_context` | 프로젝트 컨텍스트 로드 | 불필요 |
31
31
  | `deliberation_browser_llm_tabs` | 브라우저 LLM 탭 목록 (웹 기반 LLM 참여용) | 불필요 |
32
- | `deliberation_clipboard_prepare_turn` | 클립보드 기반 준비 (프롬프트 생성) | 선택적* |
33
- | `deliberation_clipboard_submit_turn` | 클립보드 기반 턴 제출 (응답 붙여넣기) | 선택적* |
34
- | `deliberation_route_turn` | 현재 차례 speaker의 transport(CLI/clipboard/manual)를 자동 라우팅 | 선택적* |
32
+ | `deliberation_route_turn` | 현재 차례 speaker의 transport(CLI/browser_auto/manual) 자동 라우팅 | 선택적* |
35
33
  | `deliberation_respond` | 현재 차례의 응답 제출 | 선택적* |
36
34
  | `deliberation_history` | 전체 토론 기록 조회 | 선택적* |
37
35
  | `deliberation_synthesize` | 합성 보고서 생성 및 토론 완료 | 선택적* |
@@ -52,21 +50,38 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
52
50
  - "deliberation", "deliberate", "토론", "debate"
53
51
  - "deliberation 시작", "토론 시작", "토론해", "토론하자"
54
52
  - "deliberation_start", "deliberation_respond", "deliberation_route_turn"
55
- - "speaker candidates", "브라우저 LLM", "clipboard submit"
53
+ - "speaker candidates", "브라우저 LLM"
54
+ - "크롬", "브라우저", "웹 LLM", "chrome", "browser LLM"
56
55
  - "{주제} 토론", "{주제} deliberation"
57
56
 
58
57
  ## 워크플로우
59
58
 
60
59
  ### A. 사용자 선택형 진행 (권장)
61
60
  1. `deliberation_speaker_candidates` → 참가 가능한 CLI/브라우저 speaker 확인
62
- 2. (선택) `deliberation_browser_llm_tabs` LLM 점검
63
- 3. `deliberation_start` (speakers 명시) → session_id 획득
64
- 4. `deliberation_route_turn` → 현재 차례 speaker transport 확인 + turn_id 확보
65
- 5. 라우팅 결과에 따라 제출:
66
- - CLI speaker: `deliberation_respond(session_id, speaker, content, turn_id)`
67
- - Browser speaker: `deliberation_clipboard_prepare_turn` → 응답 복사 → `deliberation_clipboard_submit_turn(session_id, speaker, turn_id)`
68
- 6. 반복 후 `deliberation_synthesize(session_id)` → 합성 완료
69
- 7. 구현이 필요하면 `deliberation-executor` 스킬로 handoff
61
+ 2. **AskUserQuestion으로 참가자 선택** — 감지된 CLI/브라우저 speaker 목록을 `multiSelect: true`로 제시하여 사용자가 원하는 참가자만 체크. 예:
62
+ ```
63
+ AskUserQuestion({
64
+ questions: [{
65
+ question: "토론에 참여할 speaker 선택하세요",
66
+ header: "참가자",
67
+ multiSelect: true,
68
+ options: [
69
+ { label: "claude", description: "CLI (자동 응답)" },
70
+ { label: "codex", description: "CLI (자동 응답)" },
71
+ { label: "gemini", description: "CLI (자동 응답)" },
72
+ { label: "web-chatgpt-1", description: "⚡자동 (CDP 자동 연결)" },
73
+ { label: "web-claude-1", description: "⚡자동 (CDP 자동 연결)" },
74
+ { label: "web-gemini-1", description: "⚡자동 (CDP 자동 연결)" }
75
+ ]
76
+ }]
77
+ })
78
+ ```
79
+ 3. `deliberation_start` (선택된 speakers 전달) → session_id 획득
80
+ 4. `deliberation_route_turn` → 현재 차례 speaker transport 자동 결정
81
+ - CLI speaker → 자동 응답
82
+ - browser_auto → CDP로 자동 전송/수집
83
+ 5. 반복 후 `deliberation_synthesize(session_id)` → 합성 완료
84
+ 6. 구현이 필요하면 `deliberation-executor` 스킬로 handoff
70
85
  예: "session_id {id} 합의안 구현해줘"
71
86
 
72
87
  ### B. 병렬 세션 운영
@@ -100,6 +115,13 @@ bash deliberation-monitor.sh <session_id>
100
115
  bash deliberation-monitor.sh --tmux
101
116
  ```
102
117
 
118
+ ### E. 브라우저 LLM 자동 연결 (CDP Auto-Activation)
119
+ - 브라우저 LLM speaker가 선택되면 CDP(Chrome DevTools Protocol)가 자동으로 활성화됩니다.
120
+ - macOS에서는 Chrome이 실행되지 않은 경우 `--remote-debugging-port=9222`로 자동 실행을 시도합니다.
121
+ - **Chrome이 이미 CDP 없이 실행 중인 경우**: Chrome을 완전히 종료한 후 다시 시도해야 합니다. (최초 1회만 필요)
122
+ - CDP 연결 성공 시 모든 브라우저 speaker는 ⚡자동 모드로 동작합니다.
123
+ - Windows/Linux에서는 사용자가 직접 Chrome을 `--remote-debugging-port=9222`로 실행해야 합니다.
124
+
103
125
  ## 역할 규칙
104
126
 
105
127
  ### 역할 예시 A: 비판적 분석가
@@ -130,6 +152,6 @@ bash deliberation-monitor.sh --tmux
130
152
  1. 여러 deliberation을 동시에 병렬 진행 가능
131
153
  2. session_id는 `deliberation_start` 응답에서 확인
132
154
  3. 토론 결과는 Obsidian vault에 자동 아카이브 (프로젝트 폴더 존재 시)
133
- 4. `deliberation-{session_id}.md`가 프로젝트 루트에 실시간 동기화됨
155
+ 4. 실시간 sync 파일은 state 디렉토리에 저장되며 완료 시 자동 삭제됨 (프로젝트 루트 오염 없음)
134
156
  5. `Transport closed` 발생 시 현재 CLI 세션 재시작 후 재시도 (stdio 연결은 세션 바인딩)
135
157
  6. 멀티 세션 운영 중 `pkill -f mcp-deliberation` 사용 금지 (다른 세션 연결까지 끊길 수 있음)