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