@dmsdc-ai/aigentry-deliberation 0.0.24 → 0.0.26

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/index.js CHANGED
@@ -56,6 +56,12 @@ Usage:
56
56
  * deliberation_browser_auto_turn 브라우저 LLM에 자동으로 턴을 전송하고 응답을 수집 (CDP 기반)
57
57
  * deliberation_cli_auto_turn CLI speaker에 자동으로 턴을 전송하고 응답을 수집
58
58
  * deliberation_request_review 코드 리뷰 요청 (CLI 리뷰어 자동 호출, sync/async 모드)
59
+ * decision_start 새 의사결정 세션 시작 (템플릿 지원)
60
+ * decision_status 의사결정 세션 상태 조회
61
+ * decision_respond user_probe 갈등 질문에 대한 사용자 응답 제출
62
+ * decision_resume 일시 중지된 세션 재개
63
+ * decision_history 과거 의사결정 기록 조회
64
+ * decision_templates Micro-Decision 템플릿 목록
59
65
  */
60
66
 
61
67
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -68,6 +74,13 @@ import { fileURLToPath } from "url";
68
74
  import os from "os";
69
75
  import { OrchestratedBrowserPort } from "./browser-control-port.js";
70
76
  import { getModelSelectionForTurn } from "./model-router.js";
77
+ import {
78
+ DECISION_STAGES, STAGE_TRANSITIONS,
79
+ createDecisionSession, advanceStage, buildConflictMap,
80
+ parseOpinionFromResponse, buildOpinionPrompt,
81
+ generateConflictQuestions, buildSynthesis, buildActionPlan,
82
+ loadTemplates, matchTemplate,
83
+ } from "./decision-engine.js";
71
84
 
72
85
  // ── Paths ──────────────────────────────────────────────────────
73
86
 
@@ -1220,17 +1233,20 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
1220
1233
 
1221
1234
  // CDP auto-detection: probe endpoints for matching tabs
1222
1235
  const cdpEndpoints = resolveCdpEndpoints();
1223
- const cdpTabs = [];
1236
+ const cdpTabsMap = new Map(); // dedupe by tab ID (multiple endpoints may return same tabs)
1224
1237
  for (const endpoint of cdpEndpoints) {
1225
1238
  try {
1226
1239
  const tabs = await fetchJson(endpoint, 2000);
1227
1240
  if (Array.isArray(tabs)) {
1228
1241
  for (const t of tabs) {
1229
- if (t.type === "page" && t.url) cdpTabs.push(t);
1242
+ if (t.type === "page" && t.url && t.id && !cdpTabsMap.has(t.id)) {
1243
+ cdpTabsMap.set(t.id, t);
1244
+ }
1230
1245
  }
1231
1246
  }
1232
1247
  } catch { /* endpoint not reachable */ }
1233
1248
  }
1249
+ const cdpTabs = [...cdpTabsMap.values()];
1234
1250
 
1235
1251
  // Match CDP tabs with discovered browser candidates
1236
1252
  for (const candidate of candidates) {
@@ -1244,7 +1260,7 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
1244
1260
  String(t.url || "").startsWith("chrome-extension://") &&
1245
1261
  String(t.title || "").toLowerCase().includes(candidateTitle)
1246
1262
  );
1247
- if (matches.length === 1) {
1263
+ if (matches.length >= 1) {
1248
1264
  candidate.cdp_available = true;
1249
1265
  candidate.cdp_tab_id = matches[0].id;
1250
1266
  candidate.cdp_ws_url = matches[0].webSocketDebuggerUrl;
@@ -1262,7 +1278,7 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
1262
1278
  return new URL(t.url).hostname.toLowerCase() === candidateHost;
1263
1279
  } catch { return false; }
1264
1280
  });
1265
- if (matches.length === 1) {
1281
+ if (matches.length >= 1) {
1266
1282
  candidate.cdp_available = true;
1267
1283
  candidate.cdp_tab_id = matches[0].id;
1268
1284
  candidate.cdp_ws_url = matches[0].webSocketDebuggerUrl;
@@ -1462,7 +1478,7 @@ function resolveTransportForSpeaker(state, speaker) {
1462
1478
  // CLI-specific invocation flags for non-interactive execution
1463
1479
  const CLI_INVOCATION_HINTS = {
1464
1480
  claude: { cmd: "claude", flags: '-p --output-format text', example: 'CLAUDECODE= claude -p --output-format text "프롬프트"', envPrefix: 'CLAUDECODE=', modelFlag: '--model', provider: 'claude' },
1465
- codex: { cmd: "codex", flags: 'exec', example: 'codex exec "프롬프트"', modelFlag: '--model', provider: 'chatgpt' },
1481
+ codex: { cmd: "codex", flags: 'exec --model gpt-5.4-codex', example: 'codex exec --model gpt-5.4-codex "프롬프트"', modelFlag: '--model', defaultModel: 'gpt-5.4-codex', provider: 'chatgpt' },
1466
1482
  gemini: { cmd: "gemini", flags: '', example: 'gemini "프롬프트"', modelFlag: '--model', provider: 'gemini' },
1467
1483
  aider: { cmd: "aider", flags: '--message', example: 'aider --message "프롬프트"', modelFlag: '--model', provider: 'chatgpt' },
1468
1484
  cursor: { cmd: "cursor", flags: '', example: 'cursor "프롬프트"', modelFlag: null, provider: 'chatgpt' },
@@ -1846,7 +1862,9 @@ function tmuxWindowCount(name) {
1846
1862
 
1847
1863
  function buildTmuxAttachCommand(sessionId) {
1848
1864
  const winName = tmuxWindowName(sessionId);
1849
- return `tmux attach -t ${shellQuote(TMUX_SESSION)} \\; select-window -t ${shellQuote(`${TMUX_SESSION}:${winName}`)}`;
1865
+ // Use grouped session (new-session -t) so each terminal has independent active window.
1866
+ // This prevents window-switching conflicts when multiple deliberations run concurrently.
1867
+ return `tmux new-session -t ${shellQuote(TMUX_SESSION)} \\; select-window -t ${shellQuote(`${TMUX_SESSION}:${winName}`)}`;
1850
1868
  }
1851
1869
 
1852
1870
  function listPhysicalTerminalWindowIds() {
@@ -1887,16 +1905,31 @@ function listPhysicalTerminalWindowIds() {
1887
1905
 
1888
1906
  function openPhysicalTerminal(sessionId) {
1889
1907
  const winName = tmuxWindowName(sessionId);
1890
- const attachCmd = `tmux attach -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
1908
+ // Use grouped session (new-session -t) for independent active window per client
1909
+ const attachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
1891
1910
 
1892
- // If a terminal is already attached to this tmux session, just switch to the right window
1911
+ // If a terminal is already attached, open a NEW grouped session instead of
1912
+ // select-window (which would hijack all attached clients' views).
1913
+ // Grouped sessions share windows but each has independent active window tracking.
1893
1914
  if (tmuxHasAttachedClients(TMUX_SESSION)) {
1894
- try {
1895
- execFileSync("tmux", ["select-window", "-t", `${TMUX_SESSION}:${winName}`], {
1896
- stdio: "ignore",
1897
- windowsHide: true,
1898
- });
1899
- } catch { /* window might not exist yet */ }
1915
+ if (process.platform === "darwin") {
1916
+ const groupAttachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
1917
+ try {
1918
+ execFileSync(
1919
+ "osascript",
1920
+ [
1921
+ "-e", 'tell application "Terminal"',
1922
+ "-e", "activate",
1923
+ "-e", `do script ${appleScriptQuote(groupAttachCmd)}`,
1924
+ "-e", "end tell",
1925
+ ],
1926
+ { encoding: "utf-8" }
1927
+ );
1928
+ return { opened: true, windowIds: [] };
1929
+ } catch { /* fall through to default behavior */ }
1930
+ }
1931
+ // Non-macOS or fallback: don't force select-window, just report success
1932
+ // The monitor window already exists in tmux; user can switch manually
1900
1933
  return { opened: true, windowIds: [] };
1901
1934
  }
1902
1935
 
@@ -2613,10 +2646,10 @@ server.tool(
2613
2646
  : physicalOpened
2614
2647
  ? isWin
2615
2648
  ? `\n🖥️ 모니터 터미널 오픈됨 (Windows Terminal)`
2616
- : `\n🖥️ 모니터 터미널 오픈됨: tmux attach -t ${TMUX_SESSION}`
2649
+ : `\n🖥️ 모니터 터미널 오픈됨: tmux new-session -t ${TMUX_SESSION}`
2617
2650
  : isWin
2618
2651
  ? `\n⚠️ 모니터 터미널 자동 오픈 실패`
2619
- : `\n⚠️ tmux 윈도우는 생성됐지만 외부 터미널 자동 오픈 실패. 수동 실행: tmux attach -t ${TMUX_SESSION}`;
2652
+ : `\n⚠️ tmux 윈도우는 생성됐지만 외부 터미널 자동 오픈 실패. 수동 실행: tmux new-session -t ${TMUX_SESSION}`;
2620
2653
  const manualNotDetected = hasManualSpeakers
2621
2654
  ? speakerOrder.filter(s => !candidateSnapshot.candidates.some(c => c.speaker === s))
2622
2655
  : [];
@@ -3047,7 +3080,7 @@ server.tool(
3047
3080
  child.stdin.end();
3048
3081
  break;
3049
3082
  case "codex":
3050
- child = spawn("codex", ["exec", turnPrompt], { env, windowsHide: true });
3083
+ child = spawn("codex", ["exec", "--model", "gpt-5.4-codex", turnPrompt], { env, windowsHide: true });
3051
3084
  break;
3052
3085
  case "gemini":
3053
3086
  child = spawn("gemini", ["-p", turnPrompt], { env, windowsHide: true });
@@ -3756,6 +3789,430 @@ server.tool(
3756
3789
  })
3757
3790
  );
3758
3791
 
3792
+ // ── Decision Engine Tools ─────────────────────────────────────
3793
+
3794
+ server.tool(
3795
+ "decision_start",
3796
+ "새 의사결정 세션을 시작합니다. 여러 LLM이 독립적으로 의견을 제시하고 갈등을 가시화합니다.",
3797
+ {
3798
+ problem: z.string().describe("의사결정 문제 (예: 'JWT vs Session 인증 방식 선택')"),
3799
+ options: z.preprocess(
3800
+ (v) => (typeof v === "string" ? JSON.parse(v) : v),
3801
+ z.array(z.string()).optional()
3802
+ ).describe("선택지 목록 (예: ['JWT', 'Session', 'OAuth2'])"),
3803
+ criteria: z.preprocess(
3804
+ (v) => (typeof v === "string" ? JSON.parse(v) : v),
3805
+ z.array(z.string()).optional()
3806
+ ).describe("평가 기준 (미지정 시 템플릿에서 자동 로드)"),
3807
+ template: z.string().optional().describe("Micro-decision 템플릿 ID (lib-compare, arch-decision, pr-priority, naming-convention, tradeoff, risk-approval)"),
3808
+ speakers: z.preprocess(
3809
+ (v) => (typeof v === "string" ? JSON.parse(v) : v),
3810
+ z.array(z.string().trim().min(1).max(64)).min(2).optional()
3811
+ ).describe("참여 LLM 목록 (최소 2명, 예: ['claude', 'codex', 'gemini'])"),
3812
+ },
3813
+ safeToolHandler("decision_start", async ({ problem, options, criteria, template, speakers }) => {
3814
+ // Auto-discover speakers if not provided
3815
+ if (!speakers || speakers.length === 0) {
3816
+ const candidateSnapshot = await collectSpeakerCandidates({ include_cli: true, include_browser: false });
3817
+ speakers = candidateSnapshot.candidates
3818
+ .filter(c => c.type === "cli" && checkCliLiveness(c.speaker))
3819
+ .map(c => c.speaker)
3820
+ .slice(0, 4);
3821
+ if (speakers.length < 2) {
3822
+ return { content: [{ type: "text", text: "❌ 의사결정에 최소 2명의 speaker가 필요합니다. speakers를 직접 지정하세요." }] };
3823
+ }
3824
+ }
3825
+
3826
+ // Template matching
3827
+ const templates = loadTemplates();
3828
+ let matchedTemplate = null;
3829
+ if (template) {
3830
+ matchedTemplate = templates.find(t => t.id === template) || null;
3831
+ } else {
3832
+ matchedTemplate = matchTemplate(problem, templates);
3833
+ }
3834
+
3835
+ // Use template criteria if not provided
3836
+ if ((!criteria || criteria.length === 0) && matchedTemplate) {
3837
+ criteria = matchedTemplate.criteria.map(c => c.name || c);
3838
+ }
3839
+
3840
+ // Create session
3841
+ const session = createDecisionSession({
3842
+ problem,
3843
+ options: options || [],
3844
+ criteria: criteria || [],
3845
+ speakers,
3846
+ template: matchedTemplate?.id || null,
3847
+ participant_profiles: mapParticipantProfiles(speakers, [], {}),
3848
+ });
3849
+
3850
+ // Advance to parallel_opinions immediately (intake is just creation)
3851
+ advanceStage(session);
3852
+
3853
+ // Save session
3854
+ withSessionLock(session.id, () => {
3855
+ saveSession(session);
3856
+ });
3857
+
3858
+ appendRuntimeLog("INFO", `DECISION_START: ${session.id} | problem: ${problem.slice(0, 60)} | speakers: ${speakers.join(",")} | criteria: ${(criteria || []).length}`);
3859
+
3860
+ // Build opinion prompt for parallel execution
3861
+ const opinionPrompt = buildOpinionPrompt(problem, options || [], criteria || [], matchedTemplate?.id);
3862
+
3863
+ // Run parallel independent opinions using CLI auto-turn pattern
3864
+ const opinionResults = {};
3865
+ const opinionPromises = speakers.map(async (speaker) => {
3866
+ try {
3867
+ const hint = CLI_INVOCATION_HINTS[speaker] || CLI_INVOCATION_HINTS["_generic"];
3868
+ const cmd = hint?.cmd || speaker;
3869
+
3870
+ // Check liveness
3871
+ if (!checkCliLiveness(cmd)) {
3872
+ opinionResults[speaker] = { error: `CLI not available: ${cmd}` };
3873
+ return;
3874
+ }
3875
+
3876
+ // Spawn CLI with opinion prompt
3877
+ const result = await new Promise((resolve, reject) => {
3878
+ let stdout = "";
3879
+ let stderr = "";
3880
+
3881
+ const args = [];
3882
+ if (speaker === "claude") {
3883
+ args.push("-p", "--output-format", "text", opinionPrompt);
3884
+ } else if (speaker === "codex") {
3885
+ args.push("exec", opinionPrompt);
3886
+ } else if (speaker === "gemini") {
3887
+ args.push("-p", opinionPrompt);
3888
+ } else {
3889
+ const flags = hint?.flags || [];
3890
+ args.push(...flags, opinionPrompt);
3891
+ }
3892
+
3893
+ const proc = spawn(cmd, args, {
3894
+ stdio: ["pipe", "pipe", "pipe"],
3895
+ env: { ...process.env, NO_COLOR: "1" },
3896
+ timeout: 180000,
3897
+ });
3898
+
3899
+ proc.stdout?.on("data", (d) => { stdout += d.toString(); });
3900
+ proc.stderr?.on("data", (d) => { stderr += d.toString(); });
3901
+
3902
+ const timer = setTimeout(() => {
3903
+ proc.kill("SIGTERM");
3904
+ reject(new Error("timeout"));
3905
+ }, 180000);
3906
+
3907
+ proc.on("close", (code) => {
3908
+ clearTimeout(timer);
3909
+ resolve(stdout.trim() || stderr.trim());
3910
+ });
3911
+
3912
+ proc.on("error", (err) => {
3913
+ clearTimeout(timer);
3914
+ reject(err);
3915
+ });
3916
+ });
3917
+
3918
+ opinionResults[speaker] = result;
3919
+ } catch (err) {
3920
+ opinionResults[speaker] = { error: err.message };
3921
+ }
3922
+ });
3923
+
3924
+ // Wait for all opinions in parallel
3925
+ await Promise.all(opinionPromises);
3926
+
3927
+ // Parse opinions and update session
3928
+ withSessionLock(session.id, () => {
3929
+ const latest = loadSession(session.id);
3930
+ if (!latest) return;
3931
+
3932
+ for (const [speaker, result] of Object.entries(opinionResults)) {
3933
+ if (typeof result === "string") {
3934
+ latest.opinions[speaker] = parseOpinionFromResponse(speaker, result, latest.criteria);
3935
+ latest.log.push({
3936
+ round: 1,
3937
+ speaker,
3938
+ content: result,
3939
+ timestamp: new Date().toISOString(),
3940
+ channel_used: "cli_auto",
3941
+ event: "opinion",
3942
+ });
3943
+ } else {
3944
+ appendRuntimeLog("WARN", `DECISION_OPINION_FAIL: ${session.id} | ${speaker}: ${result?.error}`);
3945
+ }
3946
+ }
3947
+
3948
+ // Advance to conflict_map
3949
+ latest.stage = "conflict_map";
3950
+ latest.metadata.updated = new Date().toISOString();
3951
+
3952
+ // Build conflict map
3953
+ latest.conflicts = buildConflictMap(latest.opinions, latest.criteria);
3954
+
3955
+ // Advance to user_probe
3956
+ latest.stage = "user_probe";
3957
+ latest.metadata.updated = new Date().toISOString();
3958
+
3959
+ saveSession(latest);
3960
+ });
3961
+
3962
+ // Load updated session for response
3963
+ const updatedSession = loadSession(session.id);
3964
+ const conflictText = generateConflictQuestions(updatedSession?.conflicts || []);
3965
+ const successCount = Object.keys(updatedSession?.opinions || {}).length;
3966
+ const templateInfo = matchedTemplate ? `\n**Template:** ${matchedTemplate.name}` : "";
3967
+
3968
+ appendRuntimeLog("INFO", `DECISION_OPINIONS_COMPLETE: ${session.id} | opinions: ${successCount}/${speakers.length} | conflicts: ${(updatedSession?.conflicts || []).length}`);
3969
+
3970
+ return {
3971
+ content: [{
3972
+ type: "text",
3973
+ text: `✅ **Decision Session 시작됨**\n\n**Session:** ${session.id}\n**Problem:** ${problem}\n**Speakers:** ${speakers.join(", ")}\n**Opinions collected:** ${successCount}/${speakers.length}${templateInfo}\n**Stage:** user_probe (사용자 입력 대기)\n**Conflicts:** ${(updatedSession?.conflicts || []).length}개\n\n---\n\n${conflictText}\n\n---\n\n사용자 응답을 \`decision_respond\`로 제출하세요.`,
3974
+ }],
3975
+ };
3976
+ })
3977
+ );
3978
+
3979
+ server.tool(
3980
+ "decision_status",
3981
+ "의사결정 세션의 현재 상태를 조회합니다.",
3982
+ {
3983
+ session_id: z.string().optional().describe("세션 ID (미지정 시 활성 decision 세션 자동 선택)"),
3984
+ },
3985
+ safeToolHandler("decision_status", async ({ session_id }) => {
3986
+ // Find decision sessions
3987
+ const active = listActiveSessions().filter(s => {
3988
+ const full = loadSession(s.id);
3989
+ return full?.type === "decision";
3990
+ });
3991
+
3992
+ let resolved = session_id;
3993
+ if (!resolved) {
3994
+ if (active.length === 0) return { content: [{ type: "text", text: "활성 decision 세션이 없습니다." }] };
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")}` }] };
3997
+ }
3998
+
3999
+ const state = loadSession(resolved);
4000
+ if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
4001
+
4002
+ const opinionCount = Object.keys(state.opinions || {}).length;
4003
+ const conflictCount = (state.conflicts || []).length;
4004
+ const stageIdx = DECISION_STAGES.indexOf(state.stage);
4005
+ const progress = stageIdx >= 0 ? `${stageIdx + 1}/${DECISION_STAGES.length}` : state.stage;
4006
+
4007
+ return {
4008
+ content: [{
4009
+ type: "text",
4010
+ text: `📊 **Decision Session Status**\n\n**Session:** ${state.id}\n**Problem:** ${state.problem}\n**Stage:** ${state.stage} (${progress})\n**Status:** ${state.status}\n**Speakers:** ${(state.speakers || []).join(", ")}\n**Opinions:** ${opinionCount}/${(state.speakers || []).length}\n**Conflicts:** ${conflictCount}\n**Template:** ${state.template || "(none)"}\n**Created:** ${state.metadata?.created || ""}`,
4011
+ }],
4012
+ };
4013
+ })
4014
+ );
4015
+
4016
+ server.tool(
4017
+ "decision_respond",
4018
+ "user_probe 단계의 갈등 질문에 대한 사용자 응답을 제출합니다.",
4019
+ {
4020
+ session_id: z.string().optional().describe("세션 ID"),
4021
+ responses: z.preprocess(
4022
+ (v) => (typeof v === "string" ? JSON.parse(v) : v),
4023
+ z.array(z.string()).min(1)
4024
+ ).describe("각 갈등 질문에 대한 응답 배열 (conflict 순서대로)"),
4025
+ },
4026
+ safeToolHandler("decision_respond", async ({ session_id, responses }) => {
4027
+ // Find decision session
4028
+ const active = listActiveSessions().filter(s => {
4029
+ const full = loadSession(s.id);
4030
+ return full?.type === "decision";
4031
+ });
4032
+
4033
+ let resolved = session_id;
4034
+ if (!resolved) {
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를 지정하세요.` }] };
4038
+ }
4039
+
4040
+ let synthesisText = "";
4041
+ let actionPlan = null;
4042
+
4043
+ withSessionLock(resolved, () => {
4044
+ const state = loadSession(resolved);
4045
+ if (!state) return;
4046
+ if (state.stage !== "user_probe") {
4047
+ synthesisText = `❌ 현재 단계(${state.stage})에서는 응답을 받을 수 없습니다. user_probe 단계에서만 가능합니다.`;
4048
+ return;
4049
+ }
4050
+
4051
+ // Store user responses
4052
+ state.userProbeResponses = responses;
4053
+ state.log.push({
4054
+ event: "user_probe_response",
4055
+ responses,
4056
+ timestamp: new Date().toISOString(),
4057
+ });
4058
+
4059
+ // Advance to synthesis
4060
+ state.stage = "synthesis";
4061
+ state.metadata.updated = new Date().toISOString();
4062
+
4063
+ // Build synthesis
4064
+ state.synthesis = buildSynthesis(state);
4065
+
4066
+ // Advance to action_export
4067
+ state.stage = "action_export";
4068
+ state.metadata.updated = new Date().toISOString();
4069
+
4070
+ // Build action plan
4071
+ state.actionPlan = buildActionPlan(state);
4072
+
4073
+ // Advance to done
4074
+ state.stage = "done";
4075
+ state.status = "completed";
4076
+ state.metadata.updated = new Date().toISOString();
4077
+
4078
+ saveSession(state);
4079
+
4080
+ // Archive
4081
+ const archivePath = archiveState(state);
4082
+ cleanupSyncMarkdown(state);
4083
+
4084
+ synthesisText = state.synthesis;
4085
+ actionPlan = state.actionPlan;
4086
+
4087
+ appendRuntimeLog("INFO", `DECISION_COMPLETE: ${resolved} | conflicts_resolved: ${responses.length} | decision: ${(actionPlan?.decision || "").slice(0, 60)}`);
4088
+ });
4089
+
4090
+ if (synthesisText.startsWith("❌")) {
4091
+ return { content: [{ type: "text", text: synthesisText }] };
4092
+ }
4093
+
4094
+ const checklistText = actionPlan?.exportFormats?.checklist || "";
4095
+ return {
4096
+ content: [{
4097
+ type: "text",
4098
+ text: `✅ **Decision Complete**\n\n${synthesisText}\n\n---\n\n## Action Plan\n\n${checklistText}`,
4099
+ }],
4100
+ };
4101
+ })
4102
+ );
4103
+
4104
+ server.tool(
4105
+ "decision_resume",
4106
+ "일시 중지된 decision 세션을 재개합니다 (user_probe 단계에서 갈등 질문을 다시 표시).",
4107
+ {
4108
+ session_id: z.string().optional().describe("세션 ID"),
4109
+ },
4110
+ safeToolHandler("decision_resume", async ({ session_id }) => {
4111
+ const active = listActiveSessions().filter(s => {
4112
+ const full = loadSession(s.id);
4113
+ return full?.type === "decision";
4114
+ });
4115
+
4116
+ let resolved = session_id;
4117
+ if (!resolved) {
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")}` }] };
4121
+ }
4122
+
4123
+ const state = loadSession(resolved);
4124
+ if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
4125
+ if (state.stage !== "user_probe") {
4126
+ return { content: [{ type: "text", text: `세션이 user_probe 단계가 아닙니다 (현재: ${state.stage}). 재개할 수 없습니다.` }] };
4127
+ }
4128
+
4129
+ const conflictText = generateConflictQuestions(state.conflicts || []);
4130
+ return {
4131
+ content: [{
4132
+ type: "text",
4133
+ text: `📋 **Decision Session 재개**\n\n**Session:** ${state.id}\n**Problem:** ${state.problem}\n**Stage:** user_probe\n\n---\n\n${conflictText}\n\n---\n\n사용자 응답을 \`decision_respond\`로 제출하세요.`,
4134
+ }],
4135
+ };
4136
+ })
4137
+ );
4138
+
4139
+ server.tool(
4140
+ "decision_history",
4141
+ "과거 의사결정 기록을 조회합니다.",
4142
+ {
4143
+ session_id: z.string().optional().describe("특정 세션 ID (미지정 시 전체 목록)"),
4144
+ },
4145
+ safeToolHandler("decision_history", async ({ session_id }) => {
4146
+ if (session_id) {
4147
+ const state = loadSession(session_id);
4148
+ if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${session_id}` }] };
4149
+
4150
+ const opinionSummary = Object.entries(state.opinions || {})
4151
+ .map(([speaker, op]) => `- **${speaker}**: ${op.summary || "(none)"} (confidence: ${Math.round((op.confidence || 0.5) * 100)}%)`)
4152
+ .join("\n");
4153
+
4154
+ return {
4155
+ content: [{
4156
+ type: "text",
4157
+ text: `📜 **Decision History: ${state.id}**\n\n**Problem:** ${state.problem}\n**Status:** ${state.status}\n**Stage:** ${state.stage}\n**Template:** ${state.template || "(none)"}\n\n## Opinions\n${opinionSummary || "(none)"}\n\n## Synthesis\n${state.synthesis || "(not yet synthesized)"}\n\n## Action Plan\n${state.actionPlan?.exportFormats?.checklist || "(not yet generated)"}`,
4158
+ }],
4159
+ };
4160
+ }
4161
+
4162
+ // List all decision sessions from archives
4163
+ const projectSlug = getProjectSlug();
4164
+ const archiveDir = path.join(GLOBAL_STATE_DIR, projectSlug, "archive");
4165
+ let decisionArchives = [];
4166
+ try {
4167
+ const files = fs.readdirSync(archiveDir).filter(f => f.startsWith("decision-"));
4168
+ decisionArchives = files.map(f => {
4169
+ const match = f.match(/^decision-(.+)\.md$/);
4170
+ return match ? match[1] : f;
4171
+ });
4172
+ } catch { /* no archives */ }
4173
+
4174
+ // Also list active decision sessions
4175
+ const activeSessions = listActiveSessions().filter(s => {
4176
+ const full = loadSession(s.id);
4177
+ return full?.type === "decision";
4178
+ });
4179
+
4180
+ const activeList = activeSessions.map(s => `- 🟢 ${s.id} (${s.status})`).join("\n");
4181
+ const archiveList = decisionArchives.map(a => `- 📁 ${a}`).join("\n");
4182
+
4183
+ return {
4184
+ content: [{
4185
+ type: "text",
4186
+ text: `📜 **Decision History**\n\n## Active Sessions\n${activeList || "(none)"}\n\n## Archives\n${archiveList || "(none)"}`,
4187
+ }],
4188
+ };
4189
+ })
4190
+ );
4191
+
4192
+ server.tool(
4193
+ "decision_templates",
4194
+ "사용 가능한 Micro-Decision 템플릿 목록을 표시합니다.",
4195
+ {},
4196
+ safeToolHandler("decision_templates", async () => {
4197
+ const templates = loadTemplates();
4198
+ if (templates.length === 0) {
4199
+ return { content: [{ type: "text", text: "사용 가능한 템플릿이 없습니다." }] };
4200
+ }
4201
+
4202
+ const list = templates.map(t => {
4203
+ const criteriaList = (t.criteria || []).map(c => c.name || c.label || c).join(", ");
4204
+ return `### ${t.name} (\`${t.id}\`)\n${t.description}\n- **Criteria:** ${criteriaList}\n- **Example:** ${t.example_problem || "(none)"}`;
4205
+ }).join("\n\n");
4206
+
4207
+ return {
4208
+ content: [{
4209
+ type: "text",
4210
+ text: `📋 **Decision Templates**\n\n${list}\n\n---\n\n\`decision_start(problem: "...", template: "lib-compare")\`로 사용하세요.`,
4211
+ }],
4212
+ };
4213
+ })
4214
+ );
4215
+
3759
4216
  // ── Start ──────────────────────────────────────────────────────
3760
4217
 
3761
4218
  // Only start server when run directly (not imported for testing)
@@ -3784,4 +4241,4 @@ if (__entryFile && path.resolve(__currentFile) === __entryFile) {
3784
4241
  }
3785
4242
 
3786
4243
  // ── Test exports (used by vitest) ──
3787
- export { selectNextSpeaker, loadRolePrompt, inferSuggestedRole, parseVotes, ROLE_KEYWORDS, ROLE_HEADING_MARKERS, loadRolePresets, applyRolePreset, detectDegradationLevels, formatDegradationReport, DEGRADATION_TIERS };
4244
+ export { selectNextSpeaker, loadRolePrompt, inferSuggestedRole, parseVotes, ROLE_KEYWORDS, ROLE_HEADING_MARKERS, loadRolePresets, applyRolePreset, detectDegradationLevels, formatDegradationReport, DEGRADATION_TIERS, DECISION_STAGES, STAGE_TRANSITIONS, createDecisionSession, advanceStage, buildConflictMap, parseOpinionFromResponse, buildOpinionPrompt, generateConflictQuestions, buildSynthesis, buildActionPlan, loadTemplates, matchTemplate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-deliberation",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "MCP Deliberation Server — Multi-session AI deliberation with smart speaker ordering and persona roles",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,86 @@
1
+ {
2
+ "templates": [
3
+ {
4
+ "id": "lib-compare",
5
+ "name": "Library/Package Selection",
6
+ "description": "Compare libraries or packages for a specific use case",
7
+ "keywords": ["library", "package", "npm", "dependency", "compare", "라이브러리", "패키지", "선택"],
8
+ "criteria": [
9
+ { "name": "performance", "label": "Performance", "description": "Runtime speed and efficiency" },
10
+ { "name": "bundle_size", "label": "Bundle Size", "description": "Package size impact on build" },
11
+ { "name": "community", "label": "Community & Ecosystem", "description": "Active maintenance, documentation, community size" },
12
+ { "name": "type_support", "label": "TypeScript Support", "description": "Type definitions and type safety" },
13
+ { "name": "api_design", "label": "API Design", "description": "Developer experience and API ergonomics" }
14
+ ],
15
+ "example_problem": "Should we use date-fns or dayjs for date formatting?"
16
+ },
17
+ {
18
+ "id": "arch-decision",
19
+ "name": "Architecture Decision",
20
+ "description": "Choose between architectural approaches or patterns",
21
+ "keywords": ["architecture", "design", "pattern", "approach", "아키텍처", "설계", "구조"],
22
+ "criteria": [
23
+ { "name": "scalability", "label": "Scalability", "description": "Ability to handle growth" },
24
+ { "name": "complexity", "label": "Complexity", "description": "Implementation and maintenance complexity (lower is better)" },
25
+ { "name": "maintainability", "label": "Maintainability", "description": "Long-term maintenance burden" },
26
+ { "name": "performance", "label": "Performance", "description": "Runtime performance characteristics" },
27
+ { "name": "cost", "label": "Cost", "description": "Infrastructure and development cost" }
28
+ ],
29
+ "example_problem": "Should we use microservices or monolith for our backend?"
30
+ },
31
+ {
32
+ "id": "pr-priority",
33
+ "name": "PR/Task Prioritization",
34
+ "description": "Prioritize pull requests or tasks by importance",
35
+ "keywords": ["priority", "PR", "task", "backlog", "prioritize", "우선순위", "백로그"],
36
+ "criteria": [
37
+ { "name": "urgency", "label": "Urgency", "description": "Time sensitivity and deadline pressure" },
38
+ { "name": "impact", "label": "Impact Scope", "description": "Number of users/systems affected" },
39
+ { "name": "dependencies", "label": "Dependencies", "description": "How many other tasks depend on this" },
40
+ { "name": "risk", "label": "Risk Level", "description": "Potential for introducing issues" },
41
+ { "name": "effort", "label": "Effort", "description": "Implementation effort required (lower is better)" }
42
+ ],
43
+ "example_problem": "Which of these 5 PRs should we review first?"
44
+ },
45
+ {
46
+ "id": "naming-convention",
47
+ "name": "API/Code Naming Convention",
48
+ "description": "Decide on naming conventions for APIs, variables, or components",
49
+ "keywords": ["naming", "convention", "API", "name", "rename", "네이밍", "컨벤션", "이름"],
50
+ "criteria": [
51
+ { "name": "consistency", "label": "Consistency", "description": "Alignment with existing codebase patterns" },
52
+ { "name": "readability", "label": "Readability", "description": "How intuitive the name is" },
53
+ { "name": "standards", "label": "Standards Compliance", "description": "Alignment with industry standards/RESTful conventions" },
54
+ { "name": "brevity", "label": "Brevity", "description": "Conciseness without losing meaning" }
55
+ ],
56
+ "example_problem": "Should our endpoint be /users/:id/posts or /posts?userId=:id?"
57
+ },
58
+ {
59
+ "id": "tradeoff",
60
+ "name": "General Tradeoff Analysis",
61
+ "description": "Analyze tradeoffs between options with custom criteria",
62
+ "keywords": ["tradeoff", "trade-off", "compare", "vs", "versus", "트레이드오프", "비교"],
63
+ "criteria": [
64
+ { "name": "benefit", "label": "Expected Benefit", "description": "Positive outcomes and advantages" },
65
+ { "name": "cost", "label": "Cost/Effort", "description": "Resources required (lower is better)" },
66
+ { "name": "risk", "label": "Risk", "description": "Potential downsides (lower is better)" },
67
+ { "name": "reversibility", "label": "Reversibility", "description": "How easy to undo if wrong" }
68
+ ],
69
+ "example_problem": "Should we rewrite the auth module or patch the existing one?"
70
+ },
71
+ {
72
+ "id": "risk-approval",
73
+ "name": "Risk Approval",
74
+ "description": "Evaluate and approve/reject a risk or risky change",
75
+ "keywords": ["risk", "approve", "danger", "breaking", "리스크", "위험", "승인"],
76
+ "criteria": [
77
+ { "name": "probability", "label": "Probability", "description": "Likelihood of the risk materializing" },
78
+ { "name": "impact", "label": "Impact", "description": "Severity if the risk materializes" },
79
+ { "name": "mitigation", "label": "Mitigation Quality", "description": "Effectiveness of proposed mitigations" },
80
+ { "name": "detectability", "label": "Detectability", "description": "Ability to detect if risk occurs" },
81
+ { "name": "alternatives", "label": "Alternative Availability", "description": "Availability of safer alternatives" }
82
+ ],
83
+ "example_problem": "Should we deploy the database migration during peak hours?"
84
+ }
85
+ ]
86
+ }