@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 +16 -0
- package/browser-control-port.js +4 -0
- package/index.js +247 -28
- package/package.json +1 -1
- package/skills/deliberation/SKILL.md +17 -3
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
|
+

|
|
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
|
package/browser-control-port.js
CHANGED
|
@@ -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
|
-
//
|
|
873
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
fs.
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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:
|
|
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
|
-
//
|
|
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}
|
|
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}
|
|
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
|
|
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:
|
|
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
|
|
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
|
@@ -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으로 참가자
|
|
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.
|
|
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} 합의안 구현해줘"
|