@dmsdc-ai/aigentry-deliberation 0.0.24 → 0.0.25
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 +468 -14
- package/package.json +1 -1
- package/selectors/decision-templates.json +86 -0
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
|
|
|
@@ -1462,7 +1475,7 @@ function resolveTransportForSpeaker(state, speaker) {
|
|
|
1462
1475
|
// CLI-specific invocation flags for non-interactive execution
|
|
1463
1476
|
const CLI_INVOCATION_HINTS = {
|
|
1464
1477
|
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' },
|
|
1478
|
+
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
1479
|
gemini: { cmd: "gemini", flags: '', example: 'gemini "프롬프트"', modelFlag: '--model', provider: 'gemini' },
|
|
1467
1480
|
aider: { cmd: "aider", flags: '--message', example: 'aider --message "프롬프트"', modelFlag: '--model', provider: 'chatgpt' },
|
|
1468
1481
|
cursor: { cmd: "cursor", flags: '', example: 'cursor "프롬프트"', modelFlag: null, provider: 'chatgpt' },
|
|
@@ -1846,7 +1859,9 @@ function tmuxWindowCount(name) {
|
|
|
1846
1859
|
|
|
1847
1860
|
function buildTmuxAttachCommand(sessionId) {
|
|
1848
1861
|
const winName = tmuxWindowName(sessionId);
|
|
1849
|
-
|
|
1862
|
+
// Use grouped session (new-session -t) so each terminal has independent active window.
|
|
1863
|
+
// This prevents window-switching conflicts when multiple deliberations run concurrently.
|
|
1864
|
+
return `tmux new-session -t ${shellQuote(TMUX_SESSION)} \\; select-window -t ${shellQuote(`${TMUX_SESSION}:${winName}`)}`;
|
|
1850
1865
|
}
|
|
1851
1866
|
|
|
1852
1867
|
function listPhysicalTerminalWindowIds() {
|
|
@@ -1887,16 +1902,31 @@ function listPhysicalTerminalWindowIds() {
|
|
|
1887
1902
|
|
|
1888
1903
|
function openPhysicalTerminal(sessionId) {
|
|
1889
1904
|
const winName = tmuxWindowName(sessionId);
|
|
1890
|
-
|
|
1905
|
+
// Use grouped session (new-session -t) for independent active window per client
|
|
1906
|
+
const attachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
|
|
1891
1907
|
|
|
1892
|
-
// If a terminal is already attached
|
|
1908
|
+
// If a terminal is already attached, open a NEW grouped session instead of
|
|
1909
|
+
// select-window (which would hijack all attached clients' views).
|
|
1910
|
+
// Grouped sessions share windows but each has independent active window tracking.
|
|
1893
1911
|
if (tmuxHasAttachedClients(TMUX_SESSION)) {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1912
|
+
if (process.platform === "darwin") {
|
|
1913
|
+
const groupAttachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
|
|
1914
|
+
try {
|
|
1915
|
+
execFileSync(
|
|
1916
|
+
"osascript",
|
|
1917
|
+
[
|
|
1918
|
+
"-e", 'tell application "Terminal"',
|
|
1919
|
+
"-e", "activate",
|
|
1920
|
+
"-e", `do script ${appleScriptQuote(groupAttachCmd)}`,
|
|
1921
|
+
"-e", "end tell",
|
|
1922
|
+
],
|
|
1923
|
+
{ encoding: "utf-8" }
|
|
1924
|
+
);
|
|
1925
|
+
return { opened: true, windowIds: [] };
|
|
1926
|
+
} catch { /* fall through to default behavior */ }
|
|
1927
|
+
}
|
|
1928
|
+
// Non-macOS or fallback: don't force select-window, just report success
|
|
1929
|
+
// The monitor window already exists in tmux; user can switch manually
|
|
1900
1930
|
return { opened: true, windowIds: [] };
|
|
1901
1931
|
}
|
|
1902
1932
|
|
|
@@ -2613,10 +2643,10 @@ server.tool(
|
|
|
2613
2643
|
: physicalOpened
|
|
2614
2644
|
? isWin
|
|
2615
2645
|
? `\n🖥️ 모니터 터미널 오픈됨 (Windows Terminal)`
|
|
2616
|
-
: `\n🖥️ 모니터 터미널 오픈됨: tmux
|
|
2646
|
+
: `\n🖥️ 모니터 터미널 오픈됨: tmux new-session -t ${TMUX_SESSION}`
|
|
2617
2647
|
: isWin
|
|
2618
2648
|
? `\n⚠️ 모니터 터미널 자동 오픈 실패`
|
|
2619
|
-
: `\n⚠️ tmux 윈도우는 생성됐지만 외부 터미널 자동 오픈 실패. 수동 실행: tmux
|
|
2649
|
+
: `\n⚠️ tmux 윈도우는 생성됐지만 외부 터미널 자동 오픈 실패. 수동 실행: tmux new-session -t ${TMUX_SESSION}`;
|
|
2620
2650
|
const manualNotDetected = hasManualSpeakers
|
|
2621
2651
|
? speakerOrder.filter(s => !candidateSnapshot.candidates.some(c => c.speaker === s))
|
|
2622
2652
|
: [];
|
|
@@ -3047,7 +3077,7 @@ server.tool(
|
|
|
3047
3077
|
child.stdin.end();
|
|
3048
3078
|
break;
|
|
3049
3079
|
case "codex":
|
|
3050
|
-
child = spawn("codex", ["exec", turnPrompt], { env, windowsHide: true });
|
|
3080
|
+
child = spawn("codex", ["exec", "--model", "gpt-5.4-codex", turnPrompt], { env, windowsHide: true });
|
|
3051
3081
|
break;
|
|
3052
3082
|
case "gemini":
|
|
3053
3083
|
child = spawn("gemini", ["-p", turnPrompt], { env, windowsHide: true });
|
|
@@ -3756,6 +3786,430 @@ server.tool(
|
|
|
3756
3786
|
})
|
|
3757
3787
|
);
|
|
3758
3788
|
|
|
3789
|
+
// ── Decision Engine Tools ─────────────────────────────────────
|
|
3790
|
+
|
|
3791
|
+
server.tool(
|
|
3792
|
+
"decision_start",
|
|
3793
|
+
"새 의사결정 세션을 시작합니다. 여러 LLM이 독립적으로 의견을 제시하고 갈등을 가시화합니다.",
|
|
3794
|
+
{
|
|
3795
|
+
problem: z.string().describe("의사결정 문제 (예: 'JWT vs Session 인증 방식 선택')"),
|
|
3796
|
+
options: z.preprocess(
|
|
3797
|
+
(v) => (typeof v === "string" ? JSON.parse(v) : v),
|
|
3798
|
+
z.array(z.string()).optional()
|
|
3799
|
+
).describe("선택지 목록 (예: ['JWT', 'Session', 'OAuth2'])"),
|
|
3800
|
+
criteria: z.preprocess(
|
|
3801
|
+
(v) => (typeof v === "string" ? JSON.parse(v) : v),
|
|
3802
|
+
z.array(z.string()).optional()
|
|
3803
|
+
).describe("평가 기준 (미지정 시 템플릿에서 자동 로드)"),
|
|
3804
|
+
template: z.string().optional().describe("Micro-decision 템플릿 ID (lib-compare, arch-decision, pr-priority, naming-convention, tradeoff, risk-approval)"),
|
|
3805
|
+
speakers: z.preprocess(
|
|
3806
|
+
(v) => (typeof v === "string" ? JSON.parse(v) : v),
|
|
3807
|
+
z.array(z.string().trim().min(1).max(64)).min(2).optional()
|
|
3808
|
+
).describe("참여 LLM 목록 (최소 2명, 예: ['claude', 'codex', 'gemini'])"),
|
|
3809
|
+
},
|
|
3810
|
+
safeToolHandler("decision_start", async ({ problem, options, criteria, template, speakers }) => {
|
|
3811
|
+
// Auto-discover speakers if not provided
|
|
3812
|
+
if (!speakers || speakers.length === 0) {
|
|
3813
|
+
const candidateSnapshot = await collectSpeakerCandidates({ include_cli: true, include_browser: false });
|
|
3814
|
+
speakers = candidateSnapshot.candidates
|
|
3815
|
+
.filter(c => c.type === "cli" && checkCliLiveness(c.speaker))
|
|
3816
|
+
.map(c => c.speaker)
|
|
3817
|
+
.slice(0, 4);
|
|
3818
|
+
if (speakers.length < 2) {
|
|
3819
|
+
return { content: [{ type: "text", text: "❌ 의사결정에 최소 2명의 speaker가 필요합니다. speakers를 직접 지정하세요." }] };
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
// Template matching
|
|
3824
|
+
const templates = loadTemplates();
|
|
3825
|
+
let matchedTemplate = null;
|
|
3826
|
+
if (template) {
|
|
3827
|
+
matchedTemplate = templates.find(t => t.id === template) || null;
|
|
3828
|
+
} else {
|
|
3829
|
+
matchedTemplate = matchTemplate(problem, templates);
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
// Use template criteria if not provided
|
|
3833
|
+
if ((!criteria || criteria.length === 0) && matchedTemplate) {
|
|
3834
|
+
criteria = matchedTemplate.criteria.map(c => c.name || c);
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
// Create session
|
|
3838
|
+
const session = createDecisionSession({
|
|
3839
|
+
problem,
|
|
3840
|
+
options: options || [],
|
|
3841
|
+
criteria: criteria || [],
|
|
3842
|
+
speakers,
|
|
3843
|
+
template: matchedTemplate?.id || null,
|
|
3844
|
+
participant_profiles: mapParticipantProfiles(speakers, [], {}),
|
|
3845
|
+
});
|
|
3846
|
+
|
|
3847
|
+
// Advance to parallel_opinions immediately (intake is just creation)
|
|
3848
|
+
advanceStage(session);
|
|
3849
|
+
|
|
3850
|
+
// Save session
|
|
3851
|
+
withSessionLock(session.id, () => {
|
|
3852
|
+
saveSession(session);
|
|
3853
|
+
});
|
|
3854
|
+
|
|
3855
|
+
appendRuntimeLog("INFO", `DECISION_START: ${session.id} | problem: ${problem.slice(0, 60)} | speakers: ${speakers.join(",")} | criteria: ${(criteria || []).length}`);
|
|
3856
|
+
|
|
3857
|
+
// Build opinion prompt for parallel execution
|
|
3858
|
+
const opinionPrompt = buildOpinionPrompt(problem, options || [], criteria || [], matchedTemplate?.id);
|
|
3859
|
+
|
|
3860
|
+
// Run parallel independent opinions using CLI auto-turn pattern
|
|
3861
|
+
const opinionResults = {};
|
|
3862
|
+
const opinionPromises = speakers.map(async (speaker) => {
|
|
3863
|
+
try {
|
|
3864
|
+
const hint = CLI_INVOCATION_HINTS[speaker] || CLI_INVOCATION_HINTS["_generic"];
|
|
3865
|
+
const cmd = hint?.cmd || speaker;
|
|
3866
|
+
|
|
3867
|
+
// Check liveness
|
|
3868
|
+
if (!checkCliLiveness(cmd)) {
|
|
3869
|
+
opinionResults[speaker] = { error: `CLI not available: ${cmd}` };
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// Spawn CLI with opinion prompt
|
|
3874
|
+
const result = await new Promise((resolve, reject) => {
|
|
3875
|
+
let stdout = "";
|
|
3876
|
+
let stderr = "";
|
|
3877
|
+
|
|
3878
|
+
const args = [];
|
|
3879
|
+
if (speaker === "claude") {
|
|
3880
|
+
args.push("-p", "--output-format", "text", opinionPrompt);
|
|
3881
|
+
} else if (speaker === "codex") {
|
|
3882
|
+
args.push("exec", opinionPrompt);
|
|
3883
|
+
} else if (speaker === "gemini") {
|
|
3884
|
+
args.push("-p", opinionPrompt);
|
|
3885
|
+
} else {
|
|
3886
|
+
const flags = hint?.flags || [];
|
|
3887
|
+
args.push(...flags, opinionPrompt);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
const proc = spawn(cmd, args, {
|
|
3891
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3892
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
3893
|
+
timeout: 180000,
|
|
3894
|
+
});
|
|
3895
|
+
|
|
3896
|
+
proc.stdout?.on("data", (d) => { stdout += d.toString(); });
|
|
3897
|
+
proc.stderr?.on("data", (d) => { stderr += d.toString(); });
|
|
3898
|
+
|
|
3899
|
+
const timer = setTimeout(() => {
|
|
3900
|
+
proc.kill("SIGTERM");
|
|
3901
|
+
reject(new Error("timeout"));
|
|
3902
|
+
}, 180000);
|
|
3903
|
+
|
|
3904
|
+
proc.on("close", (code) => {
|
|
3905
|
+
clearTimeout(timer);
|
|
3906
|
+
resolve(stdout.trim() || stderr.trim());
|
|
3907
|
+
});
|
|
3908
|
+
|
|
3909
|
+
proc.on("error", (err) => {
|
|
3910
|
+
clearTimeout(timer);
|
|
3911
|
+
reject(err);
|
|
3912
|
+
});
|
|
3913
|
+
});
|
|
3914
|
+
|
|
3915
|
+
opinionResults[speaker] = result;
|
|
3916
|
+
} catch (err) {
|
|
3917
|
+
opinionResults[speaker] = { error: err.message };
|
|
3918
|
+
}
|
|
3919
|
+
});
|
|
3920
|
+
|
|
3921
|
+
// Wait for all opinions in parallel
|
|
3922
|
+
await Promise.all(opinionPromises);
|
|
3923
|
+
|
|
3924
|
+
// Parse opinions and update session
|
|
3925
|
+
withSessionLock(session.id, () => {
|
|
3926
|
+
const latest = loadSession(session.id);
|
|
3927
|
+
if (!latest) return;
|
|
3928
|
+
|
|
3929
|
+
for (const [speaker, result] of Object.entries(opinionResults)) {
|
|
3930
|
+
if (typeof result === "string") {
|
|
3931
|
+
latest.opinions[speaker] = parseOpinionFromResponse(speaker, result, latest.criteria);
|
|
3932
|
+
latest.log.push({
|
|
3933
|
+
round: 1,
|
|
3934
|
+
speaker,
|
|
3935
|
+
content: result,
|
|
3936
|
+
timestamp: new Date().toISOString(),
|
|
3937
|
+
channel_used: "cli_auto",
|
|
3938
|
+
event: "opinion",
|
|
3939
|
+
});
|
|
3940
|
+
} else {
|
|
3941
|
+
appendRuntimeLog("WARN", `DECISION_OPINION_FAIL: ${session.id} | ${speaker}: ${result?.error}`);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
// Advance to conflict_map
|
|
3946
|
+
latest.stage = "conflict_map";
|
|
3947
|
+
latest.metadata.updated = new Date().toISOString();
|
|
3948
|
+
|
|
3949
|
+
// Build conflict map
|
|
3950
|
+
latest.conflicts = buildConflictMap(latest.opinions, latest.criteria);
|
|
3951
|
+
|
|
3952
|
+
// Advance to user_probe
|
|
3953
|
+
latest.stage = "user_probe";
|
|
3954
|
+
latest.metadata.updated = new Date().toISOString();
|
|
3955
|
+
|
|
3956
|
+
saveSession(latest);
|
|
3957
|
+
});
|
|
3958
|
+
|
|
3959
|
+
// Load updated session for response
|
|
3960
|
+
const updatedSession = loadSession(session.id);
|
|
3961
|
+
const conflictText = generateConflictQuestions(updatedSession?.conflicts || []);
|
|
3962
|
+
const successCount = Object.keys(updatedSession?.opinions || {}).length;
|
|
3963
|
+
const templateInfo = matchedTemplate ? `\n**Template:** ${matchedTemplate.name}` : "";
|
|
3964
|
+
|
|
3965
|
+
appendRuntimeLog("INFO", `DECISION_OPINIONS_COMPLETE: ${session.id} | opinions: ${successCount}/${speakers.length} | conflicts: ${(updatedSession?.conflicts || []).length}`);
|
|
3966
|
+
|
|
3967
|
+
return {
|
|
3968
|
+
content: [{
|
|
3969
|
+
type: "text",
|
|
3970
|
+
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\`로 제출하세요.`,
|
|
3971
|
+
}],
|
|
3972
|
+
};
|
|
3973
|
+
})
|
|
3974
|
+
);
|
|
3975
|
+
|
|
3976
|
+
server.tool(
|
|
3977
|
+
"decision_status",
|
|
3978
|
+
"의사결정 세션의 현재 상태를 조회합니다.",
|
|
3979
|
+
{
|
|
3980
|
+
session_id: z.string().optional().describe("세션 ID (미지정 시 활성 decision 세션 자동 선택)"),
|
|
3981
|
+
},
|
|
3982
|
+
safeToolHandler("decision_status", async ({ session_id }) => {
|
|
3983
|
+
// Find decision sessions
|
|
3984
|
+
const active = listActiveSessions().filter(s => {
|
|
3985
|
+
const full = loadSession(s.id);
|
|
3986
|
+
return full?.type === "decision";
|
|
3987
|
+
});
|
|
3988
|
+
|
|
3989
|
+
let resolved = session_id;
|
|
3990
|
+
if (!resolved) {
|
|
3991
|
+
if (active.length === 0) return { content: [{ type: "text", text: "활성 decision 세션이 없습니다." }] };
|
|
3992
|
+
if (active.length === 1) resolved = active[0].id;
|
|
3993
|
+
else return { content: [{ type: "text", text: `여러 decision 세션이 진행 중입니다. session_id를 지정하세요:\n${active.map(s => `- ${s.id}`).join("\n")}` }] };
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
const state = loadSession(resolved);
|
|
3997
|
+
if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
|
|
3998
|
+
|
|
3999
|
+
const opinionCount = Object.keys(state.opinions || {}).length;
|
|
4000
|
+
const conflictCount = (state.conflicts || []).length;
|
|
4001
|
+
const stageIdx = DECISION_STAGES.indexOf(state.stage);
|
|
4002
|
+
const progress = stageIdx >= 0 ? `${stageIdx + 1}/${DECISION_STAGES.length}` : state.stage;
|
|
4003
|
+
|
|
4004
|
+
return {
|
|
4005
|
+
content: [{
|
|
4006
|
+
type: "text",
|
|
4007
|
+
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 || ""}`,
|
|
4008
|
+
}],
|
|
4009
|
+
};
|
|
4010
|
+
})
|
|
4011
|
+
);
|
|
4012
|
+
|
|
4013
|
+
server.tool(
|
|
4014
|
+
"decision_respond",
|
|
4015
|
+
"user_probe 단계의 갈등 질문에 대한 사용자 응답을 제출합니다.",
|
|
4016
|
+
{
|
|
4017
|
+
session_id: z.string().optional().describe("세션 ID"),
|
|
4018
|
+
responses: z.preprocess(
|
|
4019
|
+
(v) => (typeof v === "string" ? JSON.parse(v) : v),
|
|
4020
|
+
z.array(z.string()).min(1)
|
|
4021
|
+
).describe("각 갈등 질문에 대한 응답 배열 (conflict 순서대로)"),
|
|
4022
|
+
},
|
|
4023
|
+
safeToolHandler("decision_respond", async ({ session_id, responses }) => {
|
|
4024
|
+
// Find decision session
|
|
4025
|
+
const active = listActiveSessions().filter(s => {
|
|
4026
|
+
const full = loadSession(s.id);
|
|
4027
|
+
return full?.type === "decision";
|
|
4028
|
+
});
|
|
4029
|
+
|
|
4030
|
+
let resolved = session_id;
|
|
4031
|
+
if (!resolved) {
|
|
4032
|
+
if (active.length === 1) resolved = active[0].id;
|
|
4033
|
+
else if (active.length === 0) return { content: [{ type: "text", text: "활성 decision 세션이 없습니다." }] };
|
|
4034
|
+
else return { content: [{ type: "text", text: `여러 decision 세션이 진행 중입니다. session_id를 지정하세요.` }] };
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
let synthesisText = "";
|
|
4038
|
+
let actionPlan = null;
|
|
4039
|
+
|
|
4040
|
+
withSessionLock(resolved, () => {
|
|
4041
|
+
const state = loadSession(resolved);
|
|
4042
|
+
if (!state) return;
|
|
4043
|
+
if (state.stage !== "user_probe") {
|
|
4044
|
+
synthesisText = `❌ 현재 단계(${state.stage})에서는 응답을 받을 수 없습니다. user_probe 단계에서만 가능합니다.`;
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
// Store user responses
|
|
4049
|
+
state.userProbeResponses = responses;
|
|
4050
|
+
state.log.push({
|
|
4051
|
+
event: "user_probe_response",
|
|
4052
|
+
responses,
|
|
4053
|
+
timestamp: new Date().toISOString(),
|
|
4054
|
+
});
|
|
4055
|
+
|
|
4056
|
+
// Advance to synthesis
|
|
4057
|
+
state.stage = "synthesis";
|
|
4058
|
+
state.metadata.updated = new Date().toISOString();
|
|
4059
|
+
|
|
4060
|
+
// Build synthesis
|
|
4061
|
+
state.synthesis = buildSynthesis(state);
|
|
4062
|
+
|
|
4063
|
+
// Advance to action_export
|
|
4064
|
+
state.stage = "action_export";
|
|
4065
|
+
state.metadata.updated = new Date().toISOString();
|
|
4066
|
+
|
|
4067
|
+
// Build action plan
|
|
4068
|
+
state.actionPlan = buildActionPlan(state);
|
|
4069
|
+
|
|
4070
|
+
// Advance to done
|
|
4071
|
+
state.stage = "done";
|
|
4072
|
+
state.status = "completed";
|
|
4073
|
+
state.metadata.updated = new Date().toISOString();
|
|
4074
|
+
|
|
4075
|
+
saveSession(state);
|
|
4076
|
+
|
|
4077
|
+
// Archive
|
|
4078
|
+
const archivePath = archiveState(state);
|
|
4079
|
+
cleanupSyncMarkdown(state);
|
|
4080
|
+
|
|
4081
|
+
synthesisText = state.synthesis;
|
|
4082
|
+
actionPlan = state.actionPlan;
|
|
4083
|
+
|
|
4084
|
+
appendRuntimeLog("INFO", `DECISION_COMPLETE: ${resolved} | conflicts_resolved: ${responses.length} | decision: ${(actionPlan?.decision || "").slice(0, 60)}`);
|
|
4085
|
+
});
|
|
4086
|
+
|
|
4087
|
+
if (synthesisText.startsWith("❌")) {
|
|
4088
|
+
return { content: [{ type: "text", text: synthesisText }] };
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
const checklistText = actionPlan?.exportFormats?.checklist || "";
|
|
4092
|
+
return {
|
|
4093
|
+
content: [{
|
|
4094
|
+
type: "text",
|
|
4095
|
+
text: `✅ **Decision Complete**\n\n${synthesisText}\n\n---\n\n## Action Plan\n\n${checklistText}`,
|
|
4096
|
+
}],
|
|
4097
|
+
};
|
|
4098
|
+
})
|
|
4099
|
+
);
|
|
4100
|
+
|
|
4101
|
+
server.tool(
|
|
4102
|
+
"decision_resume",
|
|
4103
|
+
"일시 중지된 decision 세션을 재개합니다 (user_probe 단계에서 갈등 질문을 다시 표시).",
|
|
4104
|
+
{
|
|
4105
|
+
session_id: z.string().optional().describe("세션 ID"),
|
|
4106
|
+
},
|
|
4107
|
+
safeToolHandler("decision_resume", async ({ session_id }) => {
|
|
4108
|
+
const active = listActiveSessions().filter(s => {
|
|
4109
|
+
const full = loadSession(s.id);
|
|
4110
|
+
return full?.type === "decision";
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
let resolved = session_id;
|
|
4114
|
+
if (!resolved) {
|
|
4115
|
+
if (active.length === 1) resolved = active[0].id;
|
|
4116
|
+
else if (active.length === 0) return { content: [{ type: "text", text: "재개할 decision 세션이 없습니다." }] };
|
|
4117
|
+
else return { content: [{ type: "text", text: `여러 세션 중 선택하세요:\n${active.map(s => `- ${s.id}`).join("\n")}` }] };
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
const state = loadSession(resolved);
|
|
4121
|
+
if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${resolved}` }] };
|
|
4122
|
+
if (state.stage !== "user_probe") {
|
|
4123
|
+
return { content: [{ type: "text", text: `세션이 user_probe 단계가 아닙니다 (현재: ${state.stage}). 재개할 수 없습니다.` }] };
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
const conflictText = generateConflictQuestions(state.conflicts || []);
|
|
4127
|
+
return {
|
|
4128
|
+
content: [{
|
|
4129
|
+
type: "text",
|
|
4130
|
+
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\`로 제출하세요.`,
|
|
4131
|
+
}],
|
|
4132
|
+
};
|
|
4133
|
+
})
|
|
4134
|
+
);
|
|
4135
|
+
|
|
4136
|
+
server.tool(
|
|
4137
|
+
"decision_history",
|
|
4138
|
+
"과거 의사결정 기록을 조회합니다.",
|
|
4139
|
+
{
|
|
4140
|
+
session_id: z.string().optional().describe("특정 세션 ID (미지정 시 전체 목록)"),
|
|
4141
|
+
},
|
|
4142
|
+
safeToolHandler("decision_history", async ({ session_id }) => {
|
|
4143
|
+
if (session_id) {
|
|
4144
|
+
const state = loadSession(session_id);
|
|
4145
|
+
if (!state) return { content: [{ type: "text", text: `세션을 찾을 수 없습니다: ${session_id}` }] };
|
|
4146
|
+
|
|
4147
|
+
const opinionSummary = Object.entries(state.opinions || {})
|
|
4148
|
+
.map(([speaker, op]) => `- **${speaker}**: ${op.summary || "(none)"} (confidence: ${Math.round((op.confidence || 0.5) * 100)}%)`)
|
|
4149
|
+
.join("\n");
|
|
4150
|
+
|
|
4151
|
+
return {
|
|
4152
|
+
content: [{
|
|
4153
|
+
type: "text",
|
|
4154
|
+
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)"}`,
|
|
4155
|
+
}],
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
// List all decision sessions from archives
|
|
4160
|
+
const projectSlug = getProjectSlug();
|
|
4161
|
+
const archiveDir = path.join(GLOBAL_STATE_DIR, projectSlug, "archive");
|
|
4162
|
+
let decisionArchives = [];
|
|
4163
|
+
try {
|
|
4164
|
+
const files = fs.readdirSync(archiveDir).filter(f => f.startsWith("decision-"));
|
|
4165
|
+
decisionArchives = files.map(f => {
|
|
4166
|
+
const match = f.match(/^decision-(.+)\.md$/);
|
|
4167
|
+
return match ? match[1] : f;
|
|
4168
|
+
});
|
|
4169
|
+
} catch { /* no archives */ }
|
|
4170
|
+
|
|
4171
|
+
// Also list active decision sessions
|
|
4172
|
+
const activeSessions = listActiveSessions().filter(s => {
|
|
4173
|
+
const full = loadSession(s.id);
|
|
4174
|
+
return full?.type === "decision";
|
|
4175
|
+
});
|
|
4176
|
+
|
|
4177
|
+
const activeList = activeSessions.map(s => `- 🟢 ${s.id} (${s.status})`).join("\n");
|
|
4178
|
+
const archiveList = decisionArchives.map(a => `- 📁 ${a}`).join("\n");
|
|
4179
|
+
|
|
4180
|
+
return {
|
|
4181
|
+
content: [{
|
|
4182
|
+
type: "text",
|
|
4183
|
+
text: `📜 **Decision History**\n\n## Active Sessions\n${activeList || "(none)"}\n\n## Archives\n${archiveList || "(none)"}`,
|
|
4184
|
+
}],
|
|
4185
|
+
};
|
|
4186
|
+
})
|
|
4187
|
+
);
|
|
4188
|
+
|
|
4189
|
+
server.tool(
|
|
4190
|
+
"decision_templates",
|
|
4191
|
+
"사용 가능한 Micro-Decision 템플릿 목록을 표시합니다.",
|
|
4192
|
+
{},
|
|
4193
|
+
safeToolHandler("decision_templates", async () => {
|
|
4194
|
+
const templates = loadTemplates();
|
|
4195
|
+
if (templates.length === 0) {
|
|
4196
|
+
return { content: [{ type: "text", text: "사용 가능한 템플릿이 없습니다." }] };
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
const list = templates.map(t => {
|
|
4200
|
+
const criteriaList = (t.criteria || []).map(c => c.name || c.label || c).join(", ");
|
|
4201
|
+
return `### ${t.name} (\`${t.id}\`)\n${t.description}\n- **Criteria:** ${criteriaList}\n- **Example:** ${t.example_problem || "(none)"}`;
|
|
4202
|
+
}).join("\n\n");
|
|
4203
|
+
|
|
4204
|
+
return {
|
|
4205
|
+
content: [{
|
|
4206
|
+
type: "text",
|
|
4207
|
+
text: `📋 **Decision Templates**\n\n${list}\n\n---\n\n\`decision_start(problem: "...", template: "lib-compare")\`로 사용하세요.`,
|
|
4208
|
+
}],
|
|
4209
|
+
};
|
|
4210
|
+
})
|
|
4211
|
+
);
|
|
4212
|
+
|
|
3759
4213
|
// ── Start ──────────────────────────────────────────────────────
|
|
3760
4214
|
|
|
3761
4215
|
// Only start server when run directly (not imported for testing)
|
|
@@ -3784,4 +4238,4 @@ if (__entryFile && path.resolve(__currentFile) === __entryFile) {
|
|
|
3784
4238
|
}
|
|
3785
4239
|
|
|
3786
4240
|
// ── Test exports (used by vitest) ──
|
|
3787
|
-
export { selectNextSpeaker, loadRolePrompt, inferSuggestedRole, parseVotes, ROLE_KEYWORDS, ROLE_HEADING_MARKERS, loadRolePresets, applyRolePreset, detectDegradationLevels, formatDegradationReport, DEGRADATION_TIERS };
|
|
4241
|
+
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
|
@@ -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
|
+
}
|