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