@dmsdc-ai/aigentry-deliberation 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/index.js +668 -51
- package/install.js +63 -21
- package/package.json +1 -1
- package/skills/deliberation/SKILL.md +53 -0
package/README.md
CHANGED
|
@@ -111,8 +111,10 @@ Claude Code, Codex CLI, Gemini CLI의 MCP 설정을 자동 점검하고 문제
|
|
|
111
111
|
| `deliberation_browser_llm_tabs` | List browser LLM tabs |
|
|
112
112
|
| `deliberation_browser_auto_turn` | Auto-send turn to browser LLM |
|
|
113
113
|
| `deliberation_route_turn` | Route turn to appropriate transport |
|
|
114
|
+
| `deliberation_run_until_blocked` | Auto-run mixed transports until completion or a manual block |
|
|
114
115
|
| `deliberation_request_review` | Request code review |
|
|
115
116
|
| `deliberation_cli_auto_turn` | Auto-send turn to CLI speaker |
|
|
117
|
+
| `deliberation_ingest_remote_reply` | Canonical semantic ingress for remote replies with explicit source metadata |
|
|
116
118
|
| `deliberation_cli_config` | Configure CLI settings |
|
|
117
119
|
|
|
118
120
|
## Start Flow
|
|
@@ -133,10 +135,53 @@ Raw candidate tokens cannot start a deliberation.
|
|
|
133
135
|
Telepty-managed sessions are now routed through the telepty bus instead of raw PTY inject guidance.
|
|
134
136
|
|
|
135
137
|
- `deliberation_route_turn` publishes a typed `turn_request` envelope on `ws://localhost:3848/api/bus`
|
|
138
|
+
- `deliberation_run_until_blocked` can continue across `cli_respond`, `browser_auto`, and `telepty_bus` speakers until a manual block is reached
|
|
136
139
|
- transport delivery is tracked with a 5-second `inject_written` ack window
|
|
137
140
|
- semantic completion is tracked with a 60-second self-submit window
|
|
138
141
|
- `session_health` bus events are cached for operator visibility
|
|
139
142
|
- `deliberation_synthesize` validates and emits typed `deliberation_completed` envelopes for downstream automation
|
|
143
|
+
- telepty envelopes now carry top-level `version: 1` and optional `source_host`
|
|
144
|
+
|
|
145
|
+
### Cross-Machine Event Catalog
|
|
146
|
+
|
|
147
|
+
Canonical boundary split with telepty:
|
|
148
|
+
|
|
149
|
+
- **Guaranteed (daemon-emitted):** `inject_written`, `session_health`, `session_register`, `session.replaced`, `session.idle`, `thread.opened`, `thread.closed`, `handoff.*`, `message_routed`
|
|
150
|
+
- **Best-effort (bus relay only):** `turn_request`, `turn_completed`, `deliberation_completed`
|
|
151
|
+
- `kind` is the canonical event discriminator
|
|
152
|
+
- `target` identifies the telepty session target
|
|
153
|
+
- `payload.prompt` is the canonical prompt field for `turn_request`
|
|
154
|
+
- `source_host` is optional transport metadata for cross-machine tracing
|
|
155
|
+
|
|
156
|
+
### Remote Reply Ingress
|
|
157
|
+
|
|
158
|
+
If a remote participant cannot call local MCP tools directly, do **not** proxy-synthesize a reply. Use the deliberation-owned semantic ingress:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
deliberation_ingest_remote_reply(
|
|
162
|
+
session_id: "...",
|
|
163
|
+
speaker: "...",
|
|
164
|
+
turn_id: "...",
|
|
165
|
+
content: "...",
|
|
166
|
+
source_machine_id: "peer-01",
|
|
167
|
+
source_session_id: "remote-gemini-001",
|
|
168
|
+
transport_scope: "remote_mcp",
|
|
169
|
+
artifact_refs: ["results.jsonl"]
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This preserves explicit provenance instead of inferring semantics from raw bus events.
|
|
174
|
+
|
|
175
|
+
### Gemini Recovery
|
|
176
|
+
|
|
177
|
+
Canonical repair path today is still installer-based:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npx --yes --package @dmsdc-ai/aigentry-deliberation deliberation-doctor
|
|
181
|
+
npx --yes --package @dmsdc-ai/aigentry-deliberation deliberation-install
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use doctor first; if Gemini MCP registration/path/runtime drift is detected, rerun install and restart Gemini CLI.
|
|
140
185
|
|
|
141
186
|
## Experiment Retrospectives
|
|
142
187
|
|
package/index.js
CHANGED
|
@@ -160,9 +160,11 @@ const StructuredSynthesisSchema = z.object({
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
const StructuredExecutionContractSchema = z.object({
|
|
163
|
-
|
|
163
|
+
schema_version: z.number().int().positive(),
|
|
164
164
|
source_session_id: z.string().min(1),
|
|
165
|
+
deliberation_id: z.string().min(1),
|
|
165
166
|
summary: z.string(),
|
|
167
|
+
decisions: z.array(z.string()),
|
|
166
168
|
tasks: z.array(StructuredActionableTaskSchema),
|
|
167
169
|
experiment_outcome: StructuredExperimentOutcomeSchema.nullable().optional(),
|
|
168
170
|
unresolved_questions: z.array(z.string()),
|
|
@@ -173,11 +175,13 @@ const StructuredExecutionContractSchema = z.object({
|
|
|
173
175
|
});
|
|
174
176
|
|
|
175
177
|
const TeleptyEnvelopeSchema = z.object({
|
|
178
|
+
version: z.number().int().positive().optional(),
|
|
176
179
|
message_id: z.string().min(1),
|
|
177
180
|
session_id: z.string().min(1),
|
|
178
181
|
project: z.string().min(1),
|
|
179
182
|
kind: z.string().min(1),
|
|
180
183
|
source: z.string().min(1),
|
|
184
|
+
source_host: z.string().min(1).optional(),
|
|
181
185
|
target: z.string().min(1),
|
|
182
186
|
reply_to: z.string().nullable().optional(),
|
|
183
187
|
trace: z.array(z.string()),
|
|
@@ -192,12 +196,27 @@ const TeleptyTurnRequestPayloadSchema = z.object({
|
|
|
192
196
|
speaker: z.string().min(1),
|
|
193
197
|
role: z.string().nullable().optional(),
|
|
194
198
|
prompt: z.string().min(1),
|
|
199
|
+
content: z.string().min(1).describe("PTY-compatible alias for prompt field"),
|
|
195
200
|
prompt_sha1: z.string().length(40),
|
|
196
201
|
history_entries: z.number().int().nonnegative().optional(),
|
|
197
202
|
transport_timeout_ms: z.number().int().positive(),
|
|
198
203
|
semantic_timeout_ms: z.number().int().positive(),
|
|
199
204
|
});
|
|
200
205
|
|
|
206
|
+
const TeleptyTurnCompletedPayloadSchema = z.object({
|
|
207
|
+
turn_id: z.string().nullable().optional(),
|
|
208
|
+
speaker: z.string().min(1),
|
|
209
|
+
round: z.number().int().positive(),
|
|
210
|
+
max_rounds: z.number().int().positive(),
|
|
211
|
+
next_speaker: z.string().min(1),
|
|
212
|
+
next_round: z.number().int().positive(),
|
|
213
|
+
status: z.string().min(1),
|
|
214
|
+
total_responses: z.number().int().nonnegative(),
|
|
215
|
+
channel_used: z.string().nullable().optional(),
|
|
216
|
+
fallback_reason: z.string().nullable().optional(),
|
|
217
|
+
orchestrator_session_id: z.string().nullable().optional(),
|
|
218
|
+
});
|
|
219
|
+
|
|
201
220
|
const TeleptyDeliberationCompletedPayloadSchema = z.object({
|
|
202
221
|
topic: z.string(),
|
|
203
222
|
synthesis: z.string(),
|
|
@@ -207,6 +226,7 @@ const TeleptyDeliberationCompletedPayloadSchema = z.object({
|
|
|
207
226
|
|
|
208
227
|
const TELEPTY_ENVELOPE_PAYLOAD_SCHEMAS = {
|
|
209
228
|
turn_request: TeleptyTurnRequestPayloadSchema,
|
|
229
|
+
turn_completed: TeleptyTurnCompletedPayloadSchema,
|
|
210
230
|
deliberation_completed: TeleptyDeliberationCompletedPayloadSchema,
|
|
211
231
|
};
|
|
212
232
|
|
|
@@ -543,12 +563,26 @@ function hashStructuredSynthesis(structured) {
|
|
|
543
563
|
return hashPromptText(JSON.stringify(sortJsonValue(structured || null)));
|
|
544
564
|
}
|
|
545
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Build a deterministic execution contract from structured synthesis.
|
|
568
|
+
*
|
|
569
|
+
* Data model canonical roles:
|
|
570
|
+
* - structured_synthesis: human + reasoning canonical — rich context for
|
|
571
|
+
* human review (decisions rationale, experiment outcomes, full task descriptions).
|
|
572
|
+
* - execution_contract: automation canonical — minimal, deterministic task list
|
|
573
|
+
* derived from structured_synthesis via SHA-1 hash for provenance tracking.
|
|
574
|
+
* Consumers (inbox-watcher, devkit, registry, orchestrator) MUST prefer
|
|
575
|
+
* execution_contract when available; fall back to structured_synthesis only
|
|
576
|
+
* when execution_contract is absent.
|
|
577
|
+
*/
|
|
546
578
|
function buildExecutionContract({ state, structured }) {
|
|
547
579
|
if (!structured) return null;
|
|
548
580
|
return {
|
|
549
|
-
|
|
581
|
+
schema_version: 2,
|
|
550
582
|
source_session_id: state.id,
|
|
583
|
+
deliberation_id: state.id,
|
|
551
584
|
summary: structured.summary || "",
|
|
585
|
+
decisions: structured.decisions || [],
|
|
552
586
|
tasks: structured.actionable_tasks || [],
|
|
553
587
|
experiment_outcome: structured.experiment_outcome || null,
|
|
554
588
|
unresolved_questions: [],
|
|
@@ -572,13 +606,24 @@ function validateTeleptyEnvelope(envelope) {
|
|
|
572
606
|
return parsed;
|
|
573
607
|
}
|
|
574
608
|
|
|
575
|
-
function
|
|
609
|
+
function resolveTeleptySourceHost() {
|
|
610
|
+
const explicit = process.env.TELEPTY_SOURCE_HOST;
|
|
611
|
+
if (typeof explicit === "string" && explicit.trim()) {
|
|
612
|
+
return explicit.trim();
|
|
613
|
+
}
|
|
614
|
+
const hostname = os.hostname();
|
|
615
|
+
return typeof hostname === "string" && hostname.trim() ? hostname.trim() : undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function buildTeleptyEnvelope({ session_id, project, kind, source, source_host = resolveTeleptySourceHost(), target, reply_to = null, trace = [], payload, ts = new Date().toISOString(), message_id = createEnvelopeId(kind) }) {
|
|
576
619
|
return validateTeleptyEnvelope({
|
|
620
|
+
version: 1,
|
|
577
621
|
message_id,
|
|
578
622
|
session_id,
|
|
579
623
|
project,
|
|
580
624
|
kind,
|
|
581
625
|
source,
|
|
626
|
+
source_host,
|
|
582
627
|
target,
|
|
583
628
|
reply_to,
|
|
584
629
|
trace,
|
|
@@ -611,6 +656,7 @@ function buildTeleptyTurnRequestEnvelope({ state, speaker, turnId, turnPrompt, i
|
|
|
611
656
|
speaker,
|
|
612
657
|
role,
|
|
613
658
|
prompt: turnPrompt,
|
|
659
|
+
content: turnPrompt,
|
|
614
660
|
prompt_sha1: hashPromptText(turnPrompt),
|
|
615
661
|
history_entries: includeHistoryEntries,
|
|
616
662
|
transport_timeout_ms: TELEPTY_TRANSPORT_TIMEOUT_MS,
|
|
@@ -619,6 +665,35 @@ function buildTeleptyTurnRequestEnvelope({ state, speaker, turnId, turnPrompt, i
|
|
|
619
665
|
});
|
|
620
666
|
}
|
|
621
667
|
|
|
668
|
+
function buildTeleptyTurnCompletedEnvelope({ state, entry }) {
|
|
669
|
+
return buildTeleptyEnvelope({
|
|
670
|
+
session_id: state.id,
|
|
671
|
+
project: state.project || getProjectSlug(),
|
|
672
|
+
kind: "turn_completed",
|
|
673
|
+
source: `deliberation:${state.id}`,
|
|
674
|
+
target: "telepty-bus",
|
|
675
|
+
reply_to: state.orchestrator_session_id || state.id,
|
|
676
|
+
trace: [
|
|
677
|
+
`project:${state.project || getProjectSlug()}`,
|
|
678
|
+
`speaker:${entry.speaker}`,
|
|
679
|
+
`turn:${entry.turn_id || "none"}`,
|
|
680
|
+
],
|
|
681
|
+
payload: {
|
|
682
|
+
turn_id: entry.turn_id || null,
|
|
683
|
+
speaker: entry.speaker,
|
|
684
|
+
round: entry.round,
|
|
685
|
+
max_rounds: state.max_rounds,
|
|
686
|
+
next_speaker: state.current_speaker || "none",
|
|
687
|
+
next_round: state.current_round,
|
|
688
|
+
status: state.status,
|
|
689
|
+
total_responses: Array.isArray(state.log) ? state.log.length : 0,
|
|
690
|
+
channel_used: entry.channel_used || null,
|
|
691
|
+
fallback_reason: entry.fallback_reason || null,
|
|
692
|
+
orchestrator_session_id: state.orchestrator_session_id || null,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
622
697
|
function buildTeleptySynthesisEnvelope({ state, synthesis, structured, executionContract }) {
|
|
623
698
|
const derivedExecutionContract =
|
|
624
699
|
executionContract !== undefined
|
|
@@ -864,6 +939,24 @@ async function ensureTeleptyBusSubscriber() {
|
|
|
864
939
|
return result;
|
|
865
940
|
}
|
|
866
941
|
|
|
942
|
+
async function callBrainIngest(executionContract) {
|
|
943
|
+
if (!executionContract) return { ok: false, reason: "no_contract" };
|
|
944
|
+
try {
|
|
945
|
+
const inboxDir = path.join(os.homedir(), ".aigentry", "inbox");
|
|
946
|
+
if (!fs.existsSync(inboxDir)) {
|
|
947
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
948
|
+
}
|
|
949
|
+
const fileName = `handoff-${executionContract.deliberation_id}.json`;
|
|
950
|
+
const filePath = path.join(inboxDir, fileName);
|
|
951
|
+
fs.writeFileSync(filePath, JSON.stringify(executionContract, null, 2), "utf8");
|
|
952
|
+
appendRuntimeLog("INFO", `BRAIN_INGEST: wrote handoff file ${filePath}`);
|
|
953
|
+
return { ok: true, path: filePath };
|
|
954
|
+
} catch (err) {
|
|
955
|
+
appendRuntimeLog("WARN", `BRAIN_INGEST: failed to write handoff file: ${err.message}`);
|
|
956
|
+
return { ok: false, error: err.message };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
867
960
|
async function notifyTeleptyBus(event) {
|
|
868
961
|
const host = process.env.TELEPTY_HOST || "localhost";
|
|
869
962
|
const port = process.env.TELEPTY_PORT || "3848";
|
|
@@ -889,6 +982,130 @@ async function notifyTeleptyBus(event) {
|
|
|
889
982
|
}
|
|
890
983
|
}
|
|
891
984
|
|
|
985
|
+
function getDefaultOrchestratorSessionId() {
|
|
986
|
+
// Check multiple env vars that may indicate an orchestrator context
|
|
987
|
+
const candidates = [
|
|
988
|
+
process.env.TELEPTY_SESSION_ID,
|
|
989
|
+
process.env.DELIBERATION_ORCHESTRATOR_ID,
|
|
990
|
+
process.env.ORCHESTRATOR_SESSION_ID,
|
|
991
|
+
];
|
|
992
|
+
for (const value of candidates) {
|
|
993
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
994
|
+
}
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function buildTurnCompletionNotificationText(state, entry) {
|
|
999
|
+
const nextSpeaker = state.current_speaker || "none";
|
|
1000
|
+
const turnId = entry.turn_id || "(none)";
|
|
1001
|
+
if (state.status === "awaiting_synthesis") {
|
|
1002
|
+
return [
|
|
1003
|
+
`[deliberation turn complete]`,
|
|
1004
|
+
`session_id: ${state.id}`,
|
|
1005
|
+
`speaker: ${entry.speaker}`,
|
|
1006
|
+
`turn_id: ${turnId}`,
|
|
1007
|
+
`round: ${entry.round}/${state.max_rounds}`,
|
|
1008
|
+
`status: awaiting_synthesis`,
|
|
1009
|
+
`responses: ${state.log.length}`,
|
|
1010
|
+
`all rounds complete; run deliberation_synthesize(session_id: "${state.id}")`,
|
|
1011
|
+
`no further reply needed.`,
|
|
1012
|
+
].join("\n");
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return [
|
|
1016
|
+
`[deliberation turn complete]`,
|
|
1017
|
+
`session_id: ${state.id}`,
|
|
1018
|
+
`speaker: ${entry.speaker}`,
|
|
1019
|
+
`turn_id: ${turnId}`,
|
|
1020
|
+
`round: ${entry.round}/${state.max_rounds}`,
|
|
1021
|
+
`status: ${state.status}`,
|
|
1022
|
+
`next_speaker: ${nextSpeaker}`,
|
|
1023
|
+
`next_round: ${state.current_round}/${state.max_rounds}`,
|
|
1024
|
+
`responses: ${state.log.length}`,
|
|
1025
|
+
`informational notification only.`,
|
|
1026
|
+
`no further reply needed.`,
|
|
1027
|
+
].join("\n");
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async function notifyTeleptySessionInject({ targetSessionId, prompt, fromSessionId, replyToSessionId = null, host = TELEPTY_DEFAULT_HOST }) {
|
|
1031
|
+
if (!targetSessionId || !prompt) return { ok: false, error: "missing target or prompt" };
|
|
1032
|
+
const token = loadTeleptyAuthToken();
|
|
1033
|
+
if (!token) return { ok: false, error: "telepty auth token unavailable" };
|
|
1034
|
+
|
|
1035
|
+
try {
|
|
1036
|
+
const response = await fetch(`http://${host}:${TELEPTY_PORT}/api/sessions/${encodeURIComponent(targetSessionId)}/inject`, {
|
|
1037
|
+
method: "POST",
|
|
1038
|
+
headers: {
|
|
1039
|
+
"Content-Type": "application/json",
|
|
1040
|
+
"x-telepty-token": token,
|
|
1041
|
+
},
|
|
1042
|
+
body: JSON.stringify({
|
|
1043
|
+
prompt,
|
|
1044
|
+
from: fromSessionId || null,
|
|
1045
|
+
reply_to: replyToSessionId || null,
|
|
1046
|
+
deliberation_session_id: null,
|
|
1047
|
+
thread_id: null,
|
|
1048
|
+
}),
|
|
1049
|
+
});
|
|
1050
|
+
const data = await response.json().catch(() => null);
|
|
1051
|
+
if (!response.ok) {
|
|
1052
|
+
return { ok: false, error: data?.error || `HTTP ${response.status}` };
|
|
1053
|
+
}
|
|
1054
|
+
return { ok: true, inject_id: data?.inject_id || null };
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
return { ok: false, error: String(err?.message || err) };
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async function dispatchTeleptyTurnRequest({ state, speaker, prompt = null, includeHistoryEntries = 4, awaitSemantic = false }) {
|
|
1061
|
+
const { profile } = resolveTransportForSpeaker(state, speaker);
|
|
1062
|
+
const turnId = state.pending_turn_id || generateTurnId();
|
|
1063
|
+
const turnPrompt = buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries);
|
|
1064
|
+
const busReady = await ensureTeleptyBusSubscriber();
|
|
1065
|
+
const envelope = buildTeleptyTurnRequestEnvelope({
|
|
1066
|
+
state,
|
|
1067
|
+
speaker,
|
|
1068
|
+
turnId,
|
|
1069
|
+
turnPrompt,
|
|
1070
|
+
includeHistoryEntries,
|
|
1071
|
+
profile,
|
|
1072
|
+
});
|
|
1073
|
+
const pending = registerPendingTeleptyTurnRequest({ envelope, profile, speaker });
|
|
1074
|
+
const publishResult = await notifyTeleptyBus(envelope);
|
|
1075
|
+
const health = profile?.telepty_session_id ? getTeleptySessionHealth(profile.telepty_session_id) : null;
|
|
1076
|
+
|
|
1077
|
+
if (!publishResult.ok) {
|
|
1078
|
+
cleanupPendingTeleptyTurn(envelope.message_id);
|
|
1079
|
+
return {
|
|
1080
|
+
ok: false,
|
|
1081
|
+
stage: "publish",
|
|
1082
|
+
envelope,
|
|
1083
|
+
turnPrompt,
|
|
1084
|
+
publishResult,
|
|
1085
|
+
busReady,
|
|
1086
|
+
health,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const transportResult = await pending.transportPromise;
|
|
1091
|
+
let semanticResult = null;
|
|
1092
|
+
if (awaitSemantic && transportResult.ok) {
|
|
1093
|
+
semanticResult = await pending.semanticPromise;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return {
|
|
1097
|
+
ok: !awaitSemantic ? transportResult.ok : Boolean(semanticResult?.ok),
|
|
1098
|
+
stage: awaitSemantic ? (semanticResult?.ok ? "semantic" : (semanticResult?.code || "semantic_timeout")) : (transportResult.ok ? "transport" : (transportResult?.code || "transport_timeout")),
|
|
1099
|
+
envelope,
|
|
1100
|
+
turnPrompt,
|
|
1101
|
+
publishResult,
|
|
1102
|
+
transportResult,
|
|
1103
|
+
semanticResult,
|
|
1104
|
+
busReady,
|
|
1105
|
+
health,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
892
1109
|
function getArchiveDir(projectSlug = getProjectSlug()) {
|
|
893
1110
|
const slug = normalizeProjectSlug(projectSlug);
|
|
894
1111
|
const obsidianDir = path.join(OBSIDIAN_PROJECTS, slug, "deliberations");
|
|
@@ -1435,19 +1652,27 @@ function detectCallerSpeaker() {
|
|
|
1435
1652
|
const hinted = normalizeSpeaker(process.env.DELIBERATION_CALLER_SPEAKER);
|
|
1436
1653
|
if (hinted) return hinted;
|
|
1437
1654
|
|
|
1655
|
+
const envKeys = Object.keys(process.env).join(" ");
|
|
1438
1656
|
const pathHint = process.env.PATH || "";
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
if (pathHint.includes("/.codex/")) {
|
|
1657
|
+
|
|
1658
|
+
// Codex detection
|
|
1659
|
+
if (/\bCODEX_[A-Z0-9_]+\b/.test(envKeys) || pathHint.includes("/.codex/")) {
|
|
1443
1660
|
return "codex";
|
|
1444
1661
|
}
|
|
1445
1662
|
|
|
1446
|
-
|
|
1663
|
+
// Claude detection
|
|
1664
|
+
if (/\bCLAUDE_[A-Z0-9_]+\b/.test(envKeys) || pathHint.includes("/.claude/")) {
|
|
1447
1665
|
return "claude";
|
|
1448
1666
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1667
|
+
|
|
1668
|
+
// Gemini detection
|
|
1669
|
+
if (/\bGOOGLE_GENAI_[A-Z0-9_]+\b/.test(envKeys) || /\bGEMINI_[A-Z0-9_]+\b/.test(envKeys) || pathHint.includes("/.gemini/")) {
|
|
1670
|
+
return "gemini";
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Aider detection
|
|
1674
|
+
if (/\bAIDER_[A-Z0-9_]+\b/.test(envKeys) || pathHint.includes("/aider/")) {
|
|
1675
|
+
return "aider";
|
|
1451
1676
|
}
|
|
1452
1677
|
|
|
1453
1678
|
return null;
|
|
@@ -2582,6 +2807,20 @@ function cleanupSyncMarkdown(state) {
|
|
|
2582
2807
|
try { fs.unlinkSync(cwdPath); } catch { /* ignore */ }
|
|
2583
2808
|
}
|
|
2584
2809
|
|
|
2810
|
+
function formatSourceMetadataLine(meta) {
|
|
2811
|
+
if (!meta || typeof meta !== "object") return "";
|
|
2812
|
+
const parts = [];
|
|
2813
|
+
if (meta.source_machine_id) parts.push(`machine: ${meta.source_machine_id}`);
|
|
2814
|
+
if (meta.source_session_id) parts.push(`session: ${meta.source_session_id}`);
|
|
2815
|
+
if (meta.transport_scope) parts.push(`transport: ${meta.transport_scope}`);
|
|
2816
|
+
if (meta.reply_origin) parts.push(`origin: ${meta.reply_origin}`);
|
|
2817
|
+
if (meta.timestamp) parts.push(`timestamp: ${meta.timestamp}`);
|
|
2818
|
+
if (Array.isArray(meta.artifact_refs) && meta.artifact_refs.length > 0) {
|
|
2819
|
+
parts.push(`artifacts: ${meta.artifact_refs.join(", ")}`);
|
|
2820
|
+
}
|
|
2821
|
+
return parts.length > 0 ? `> _source: ${parts.join(" | ")}_\n\n` : "";
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2585
2824
|
function stateToMarkdown(s) {
|
|
2586
2825
|
const speakerOrder = buildSpeakerOrder(s.speakers, s.current_speaker, "end");
|
|
2587
2826
|
let md = `---
|
|
@@ -2628,6 +2867,7 @@ tags: [deliberation]
|
|
|
2628
2867
|
if (entry.fallback_reason) parts.push(`fallback: ${entry.fallback_reason}`);
|
|
2629
2868
|
md += `> _${parts.join(" | ")}_\n\n`;
|
|
2630
2869
|
}
|
|
2870
|
+
md += formatSourceMetadataLine(entry.source_metadata);
|
|
2631
2871
|
md += `${entry.content}\n\n`;
|
|
2632
2872
|
if (entry.attachments && entry.attachments.length > 0) {
|
|
2633
2873
|
for (const att of entry.attachments) {
|
|
@@ -2651,6 +2891,21 @@ function archiveState(state) {
|
|
|
2651
2891
|
const filename = `deliberation-${ts}-${slug}.md`;
|
|
2652
2892
|
const dest = path.join(getArchiveDir(state.project), filename);
|
|
2653
2893
|
writeTextAtomic(dest, stateToMarkdown(state));
|
|
2894
|
+
|
|
2895
|
+
// Write machine-readable execution_contract sidecar for automation consumers
|
|
2896
|
+
if (state.execution_contract) {
|
|
2897
|
+
const contractDest = dest.replace(/\.md$/, ".contract.json");
|
|
2898
|
+
writeTextAtomic(contractDest, JSON.stringify({
|
|
2899
|
+
...state.execution_contract,
|
|
2900
|
+
_meta: {
|
|
2901
|
+
archived_from: state.id,
|
|
2902
|
+
project: state.project,
|
|
2903
|
+
topic: state.topic,
|
|
2904
|
+
archived_at: new Date().toISOString(),
|
|
2905
|
+
},
|
|
2906
|
+
}, null, 2));
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2654
2909
|
return dest;
|
|
2655
2910
|
}
|
|
2656
2911
|
|
|
@@ -3296,6 +3551,21 @@ function buildCliAutoTurnFailureText({ state, speaker, hint, err, effectiveTimeo
|
|
|
3296
3551
|
);
|
|
3297
3552
|
}
|
|
3298
3553
|
|
|
3554
|
+
function buildActiveReportingSection(state, speaker) {
|
|
3555
|
+
const orchestratorSessionId = state?.orchestrator_session_id;
|
|
3556
|
+
if (!orchestratorSessionId) return "";
|
|
3557
|
+
|
|
3558
|
+
return `\n[active_reporting_rule]
|
|
3559
|
+
- After you successfully submit this turn with \`deliberation_respond(...)\`, send a brief status update to the orchestrator session.
|
|
3560
|
+
- Primary method (telepty available):
|
|
3561
|
+
\`telepty inject --from \"$TELEPTY_SESSION_ID\" --reply-to ${orchestratorSessionId} ${orchestratorSessionId} \"[deliberation status] session_id: ${state.id} speaker: ${speaker} round: ${state.current_round}/${state.max_rounds} submitted. no further reply needed.\"\`
|
|
3562
|
+
- Fallback (telepty unavailable): Include the status in your next tool call or response context.
|
|
3563
|
+
- Keep it to one short line. Do not wait for a reply.
|
|
3564
|
+
- This rule applies regardless of transport type (CLI, browser, telepty_bus).
|
|
3565
|
+
[/active_reporting_rule]
|
|
3566
|
+
`;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3299
3569
|
function buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries = 4) {
|
|
3300
3570
|
const promptBudget = getPromptBudgetForSpeaker(speaker, includeHistoryEntries);
|
|
3301
3571
|
const recent = formatRecentLogForPrompt(state, promptBudget.maxEntries, promptBudget);
|
|
@@ -3304,6 +3574,7 @@ function buildClipboardTurnPrompt(state, speaker, prompt, includeHistoryEntries
|
|
|
3304
3574
|
const noToolRule = speaker === "codex"
|
|
3305
3575
|
? `\n- Do not inspect files, run shell commands, browse, or call tools. Answer only from the provided discussion context.`
|
|
3306
3576
|
: "";
|
|
3577
|
+
const activeReportingSection = buildActiveReportingSection(state, speaker);
|
|
3307
3578
|
|
|
3308
3579
|
// Role prompt injection
|
|
3309
3580
|
const speakerRole = (state.speaker_roles || {})[speaker] || "free";
|
|
@@ -3318,7 +3589,7 @@ project: ${state.project}
|
|
|
3318
3589
|
topic: ${topic}
|
|
3319
3590
|
round: ${state.current_round}/${state.max_rounds}
|
|
3320
3591
|
target_speaker: ${speaker}
|
|
3321
|
-
required_turn: ${state.current_speaker}${roleSection}
|
|
3592
|
+
required_turn: ${state.current_speaker}${roleSection}${activeReportingSection}
|
|
3322
3593
|
|
|
3323
3594
|
[recent_log]
|
|
3324
3595
|
${recent}
|
|
@@ -3334,7 +3605,7 @@ ${recent}
|
|
|
3334
3605
|
`;
|
|
3335
3606
|
}
|
|
3336
3607
|
|
|
3337
|
-
function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel_used, fallback_reason, attachments }) {
|
|
3608
|
+
function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel_used, fallback_reason, attachments, source_metadata }) {
|
|
3338
3609
|
const resolved = resolveSessionId(session_id);
|
|
3339
3610
|
if (!resolved) {
|
|
3340
3611
|
return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
|
|
@@ -3343,7 +3614,9 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
3343
3614
|
return { content: [{ type: "text", text: multipleSessionsError() }] };
|
|
3344
3615
|
}
|
|
3345
3616
|
|
|
3346
|
-
|
|
3617
|
+
let completionState = null;
|
|
3618
|
+
let completionEntry = null;
|
|
3619
|
+
const result = withSessionLock(resolved, () => {
|
|
3347
3620
|
const state = loadSession(resolved);
|
|
3348
3621
|
if (!state || state.status !== "active") {
|
|
3349
3622
|
return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
|
|
@@ -3388,7 +3661,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
3388
3661
|
const suggestedRole = inferSuggestedRole(content);
|
|
3389
3662
|
const assignedRole = (state.speaker_roles || {})[normalizedSpeaker] || "free";
|
|
3390
3663
|
const roleDrift = assignedRole !== "free" && suggestedRole !== "free" && assignedRole !== suggestedRole;
|
|
3391
|
-
|
|
3664
|
+
const logEntry = {
|
|
3392
3665
|
round: state.current_round,
|
|
3393
3666
|
speaker: normalizedSpeaker,
|
|
3394
3667
|
content,
|
|
@@ -3400,13 +3673,15 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
3400
3673
|
suggested_next_role: suggestedRole !== "free" ? suggestedRole : undefined,
|
|
3401
3674
|
role_drift: roleDrift || undefined,
|
|
3402
3675
|
attachments: attachments || undefined,
|
|
3403
|
-
|
|
3676
|
+
source_metadata: source_metadata || undefined,
|
|
3677
|
+
};
|
|
3678
|
+
state.log.push(logEntry);
|
|
3404
3679
|
completePendingTeleptySemantic({
|
|
3405
3680
|
sessionId: state.id,
|
|
3406
3681
|
speaker: normalizedSpeaker,
|
|
3407
3682
|
turnId: state.pending_turn_id || turn_id || null,
|
|
3408
3683
|
});
|
|
3409
|
-
appendRuntimeLog("INFO", `TURN: ${state.id} | R${state.current_round} | speaker: ${normalizedSpeaker} | votes: ${votes.length > 0 ? votes.map(v => v.vote).join(",") : "none"} | channel: ${channel_used || "respond"} | attachments: ${attachments ? attachments.length : 0}`);
|
|
3684
|
+
appendRuntimeLog("INFO", `TURN: ${state.id} | R${state.current_round} | speaker: ${normalizedSpeaker} | votes: ${votes.length > 0 ? votes.map(v => v.vote).join(",") : "none"} | channel: ${channel_used || "respond"} | attachments: ${attachments ? attachments.length : 0}${source_metadata?.source_machine_id ? ` | source_machine: ${source_metadata.source_machine_id}` : ""}`);
|
|
3410
3685
|
|
|
3411
3686
|
state.current_speaker = selectNextSpeaker(state);
|
|
3412
3687
|
|
|
@@ -3434,6 +3709,17 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
3434
3709
|
state.pending_turn_id = generateTurnId();
|
|
3435
3710
|
}
|
|
3436
3711
|
|
|
3712
|
+
if (!state.orchestrator_session_id) {
|
|
3713
|
+
state.orchestrator_session_id = getDefaultOrchestratorSessionId() || null;
|
|
3714
|
+
}
|
|
3715
|
+
completionEntry = {
|
|
3716
|
+
...logEntry,
|
|
3717
|
+
turn_id: logEntry.turn_id || turn_id || null,
|
|
3718
|
+
};
|
|
3719
|
+
completionState = {
|
|
3720
|
+
...state,
|
|
3721
|
+
log: [...state.log],
|
|
3722
|
+
};
|
|
3437
3723
|
saveSession(state);
|
|
3438
3724
|
return {
|
|
3439
3725
|
content: [{
|
|
@@ -3442,6 +3728,23 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
3442
3728
|
}],
|
|
3443
3729
|
};
|
|
3444
3730
|
});
|
|
3731
|
+
|
|
3732
|
+
if (completionState && completionEntry) {
|
|
3733
|
+
const envelope = buildTeleptyTurnCompletedEnvelope({ state: completionState, entry: completionEntry });
|
|
3734
|
+
notifyTeleptyBus(envelope).catch(() => {});
|
|
3735
|
+
|
|
3736
|
+
const orchestratorSessionId = completionState.orchestrator_session_id || null;
|
|
3737
|
+
if (orchestratorSessionId) {
|
|
3738
|
+
const notificationText = buildTurnCompletionNotificationText(completionState, completionEntry);
|
|
3739
|
+
notifyTeleptySessionInject({
|
|
3740
|
+
targetSessionId: orchestratorSessionId,
|
|
3741
|
+
prompt: notificationText,
|
|
3742
|
+
fromSessionId: `deliberation:${completionState.id}`,
|
|
3743
|
+
}).catch(() => {});
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
return result;
|
|
3445
3748
|
}
|
|
3446
3749
|
|
|
3447
3750
|
// ── MCP Server ─────────────────────────────────────────────────
|
|
@@ -3525,8 +3828,11 @@ server.tool(
|
|
|
3525
3828
|
(v) => (typeof v === "string" ? v === "true" : v),
|
|
3526
3829
|
z.boolean().optional()
|
|
3527
3830
|
).describe("If true, automatically create a handoff task in the inbox when synthesis completes. Enables the Autonomous Deliberation Handoff pattern."),
|
|
3528
|
-
|
|
3529
|
-
|
|
3831
|
+
mode: z.enum(["standard", "lite"]).default("standard").describe("Deliberation mode. 'lite' caps speakers to 3 and rounds to 2 for quick decisions."),
|
|
3832
|
+
orchestrator_session_id: z.string().trim().min(1).max(128).optional()
|
|
3833
|
+
.describe("Optional telepty session ID to notify on turn completion. Defaults to TELEPTY_SESSION_ID when available."),
|
|
3834
|
+
},
|
|
3835
|
+
safeToolHandler("deliberation_start", async ({ topic, session_id, rounds, first_speaker, selection_token, speakers, speaker_instructions, require_manual_speakers, auto_discover_speakers, include_browser_speakers, participant_types, ordering_strategy, speaker_roles, role_preset, auto_execute, mode, orchestrator_session_id }) => {
|
|
3530
3836
|
// ── First-time onboarding guard ──
|
|
3531
3837
|
const config = loadDeliberationConfig();
|
|
3532
3838
|
if (!config.setup_complete) {
|
|
@@ -3649,12 +3955,22 @@ server.tool(
|
|
|
3649
3955
|
|| normalizeSpeaker(hasManualSpeakers ? selectedSpeakers?.[0] : callerSpeaker)
|
|
3650
3956
|
|| normalizeSpeaker(selectedSpeakers?.[0])
|
|
3651
3957
|
|| DEFAULT_SPEAKERS[0];
|
|
3652
|
-
|
|
3958
|
+
let speakerOrder = buildSpeakerOrder(selectedSpeakers, normalizedFirstSpeaker, "front");
|
|
3653
3959
|
|
|
3654
3960
|
if (effectiveRequireManual) {
|
|
3655
3961
|
clearSpeakerSelectionToken();
|
|
3656
3962
|
}
|
|
3657
3963
|
|
|
3964
|
+
// Lite mode: cap speakers and rounds for quick decisions
|
|
3965
|
+
if (mode === "lite") {
|
|
3966
|
+
if (speakerOrder.length > 3) {
|
|
3967
|
+
speakerOrder.splice(3);
|
|
3968
|
+
}
|
|
3969
|
+
if (rounds > 2) {
|
|
3970
|
+
rounds = 2;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3658
3974
|
// Warn if only 1 speaker — deliberation requires 2+
|
|
3659
3975
|
if (speakerOrder.length < 2) {
|
|
3660
3976
|
const candidateText = formatSpeakerCandidatesReport(candidateSnapshot);
|
|
@@ -3706,6 +4022,8 @@ server.tool(
|
|
|
3706
4022
|
speaker_roles: speaker_roles || (role_preset ? applyRolePreset(role_preset, speakerOrder) : {}),
|
|
3707
4023
|
degradation: degradationLevels,
|
|
3708
4024
|
auto_execute: auto_execute || false,
|
|
4025
|
+
mode: mode || "standard",
|
|
4026
|
+
orchestrator_session_id: orchestrator_session_id || getDefaultOrchestratorSessionId() || null,
|
|
3709
4027
|
created: new Date().toISOString(),
|
|
3710
4028
|
updated: new Date().toISOString(),
|
|
3711
4029
|
};
|
|
@@ -3994,28 +4312,22 @@ server.tool(
|
|
|
3994
4312
|
let manualFallbackPrompt = false;
|
|
3995
4313
|
|
|
3996
4314
|
if (transport === "telepty_bus") {
|
|
3997
|
-
|
|
3998
|
-
const busReady = await ensureTeleptyBusSubscriber();
|
|
3999
|
-
const envelope = buildTeleptyTurnRequestEnvelope({
|
|
4315
|
+
const dispatchResult = await dispatchTeleptyTurnRequest({
|
|
4000
4316
|
state,
|
|
4001
4317
|
speaker,
|
|
4002
|
-
|
|
4003
|
-
turnPrompt,
|
|
4318
|
+
prompt,
|
|
4004
4319
|
includeHistoryEntries: include_history_entries,
|
|
4005
|
-
|
|
4320
|
+
awaitSemantic: false,
|
|
4006
4321
|
});
|
|
4007
|
-
|
|
4008
|
-
const publishResult =
|
|
4009
|
-
const health = profile?.telepty_session_id ? getTeleptySessionHealth(profile.telepty_session_id) : null;
|
|
4322
|
+
turnPrompt = dispatchResult.turnPrompt;
|
|
4323
|
+
const { envelope, publishResult, transportResult, busReady, health } = dispatchResult;
|
|
4010
4324
|
|
|
4011
|
-
if (!
|
|
4012
|
-
cleanupPendingTeleptyTurn(envelope.message_id);
|
|
4325
|
+
if (!dispatchResult.ok && dispatchResult.stage === "publish") {
|
|
4013
4326
|
manualFallbackPrompt = true;
|
|
4014
4327
|
extra += `\n\n❌ Telepty bus publish failed: ${publishResult.error || publishResult.status || "unknown error"}\n` +
|
|
4015
4328
|
`Fallback: use manual telepty inject for this turn.`;
|
|
4016
4329
|
guidance = formatTransportGuidance("manual", state, speaker);
|
|
4017
4330
|
} else {
|
|
4018
|
-
const transportResult = await pending.transportPromise;
|
|
4019
4331
|
const healthLine = health
|
|
4020
4332
|
? `\n**Session health:** alive=${health.payload?.alive === true ? "yes" : "no"}, pid=${health.payload?.pid || "n/a"}, age=${Math.max(0, Math.round((health.age_ms || 0) / 1000))}s${health.stale ? " (stale)" : ""}`
|
|
4021
4333
|
: "";
|
|
@@ -4360,6 +4672,226 @@ async function runCliAutoTurnCore(sessionId, speaker, timeoutSec = 120) {
|
|
|
4360
4672
|
}
|
|
4361
4673
|
}
|
|
4362
4674
|
|
|
4675
|
+
async function runBrowserAutoTurnCore(sessionId, speaker, timeoutSec = 45) {
|
|
4676
|
+
const state = loadSession(sessionId);
|
|
4677
|
+
if (!state || state.status !== "active") {
|
|
4678
|
+
return { ok: false, error: "Session not active" };
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
const { transport, profile } = resolveTransportForSpeaker(state, speaker);
|
|
4682
|
+
if (transport !== "browser_auto") {
|
|
4683
|
+
return { ok: false, error: `Speaker "${speaker}" is not browser_auto type` };
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4686
|
+
const turnId = state.pending_turn_id || generateTurnId();
|
|
4687
|
+
const port = getBrowserPort();
|
|
4688
|
+
const effectiveProvider = profile?.provider || "chatgpt";
|
|
4689
|
+
const modelSelection = getModelSelectionForTurn(state, speaker, effectiveProvider);
|
|
4690
|
+
const turnPrompt = buildClipboardTurnPrompt(state, speaker, null, 3);
|
|
4691
|
+
const startTime = Date.now();
|
|
4692
|
+
|
|
4693
|
+
try {
|
|
4694
|
+
const attachResult = await port.attach(sessionId, {
|
|
4695
|
+
provider: effectiveProvider,
|
|
4696
|
+
url: profile?.url || undefined,
|
|
4697
|
+
});
|
|
4698
|
+
if (!attachResult.ok) {
|
|
4699
|
+
return { ok: false, error: `attach failed: ${attachResult.error?.message || "unknown error"}` };
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
const loginCheck = await port.checkLogin(sessionId);
|
|
4703
|
+
if (loginCheck && !loginCheck.loggedIn) {
|
|
4704
|
+
await port.detach(sessionId);
|
|
4705
|
+
return { ok: false, error: `login required: ${loginCheck.reason || "not logged in"}` };
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
if (modelSelection.model !== "default") {
|
|
4709
|
+
await port.switchModel(sessionId, modelSelection.model);
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
const sendResult = await port.sendTurnWithDegradation(sessionId, turnId, turnPrompt);
|
|
4713
|
+
if (!sendResult.ok) {
|
|
4714
|
+
await port.detach(sessionId);
|
|
4715
|
+
return { ok: false, error: `send failed: ${sendResult.error?.message || "unknown error"}` };
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
const waitResult = await port.waitTurnResult(sessionId, turnId, timeoutSec);
|
|
4719
|
+
await port.detach(sessionId);
|
|
4720
|
+
if (!waitResult.ok || !waitResult.data?.response) {
|
|
4721
|
+
return { ok: false, error: waitResult.error?.message || "no response received" };
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
submitDeliberationTurn({
|
|
4725
|
+
session_id: sessionId,
|
|
4726
|
+
speaker,
|
|
4727
|
+
content: waitResult.data.response,
|
|
4728
|
+
turn_id: turnId,
|
|
4729
|
+
channel_used: "browser_auto",
|
|
4730
|
+
});
|
|
4731
|
+
|
|
4732
|
+
return {
|
|
4733
|
+
ok: true,
|
|
4734
|
+
response: waitResult.data.response,
|
|
4735
|
+
elapsedMs: Date.now() - startTime,
|
|
4736
|
+
model: modelSelection.model,
|
|
4737
|
+
provider: effectiveProvider,
|
|
4738
|
+
};
|
|
4739
|
+
} catch (err) {
|
|
4740
|
+
try { await port.detach(sessionId); } catch {}
|
|
4741
|
+
return { ok: false, error: err?.message || String(err) };
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
async function runTeleptyBusAutoTurnCore(sessionId, speaker, includeHistoryEntries = 4) {
|
|
4746
|
+
const state = loadSession(sessionId);
|
|
4747
|
+
if (!state || state.status !== "active") {
|
|
4748
|
+
return { ok: false, error: "Session not active" };
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
const { transport } = resolveTransportForSpeaker(state, speaker);
|
|
4752
|
+
if (transport !== "telepty_bus") {
|
|
4753
|
+
return { ok: false, error: `Speaker "${speaker}" is not telepty_bus type` };
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
const startTime = Date.now();
|
|
4757
|
+
const dispatchResult = await dispatchTeleptyTurnRequest({
|
|
4758
|
+
state,
|
|
4759
|
+
speaker,
|
|
4760
|
+
includeHistoryEntries,
|
|
4761
|
+
awaitSemantic: true,
|
|
4762
|
+
});
|
|
4763
|
+
if (!dispatchResult.publishResult?.ok) {
|
|
4764
|
+
return {
|
|
4765
|
+
ok: false,
|
|
4766
|
+
blocked: true,
|
|
4767
|
+
error: dispatchResult.publishResult?.error || dispatchResult.publishResult?.status || "telepty bus publish failed",
|
|
4768
|
+
envelope: dispatchResult.envelope,
|
|
4769
|
+
turnPrompt: dispatchResult.turnPrompt,
|
|
4770
|
+
};
|
|
4771
|
+
}
|
|
4772
|
+
if (!dispatchResult.transportResult?.ok) {
|
|
4773
|
+
return {
|
|
4774
|
+
ok: false,
|
|
4775
|
+
blocked: true,
|
|
4776
|
+
error: dispatchResult.transportResult?.code || "transport timeout",
|
|
4777
|
+
envelope: dispatchResult.envelope,
|
|
4778
|
+
turnPrompt: dispatchResult.turnPrompt,
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
if (!dispatchResult.semanticResult?.ok) {
|
|
4782
|
+
return {
|
|
4783
|
+
ok: false,
|
|
4784
|
+
blocked: true,
|
|
4785
|
+
error: dispatchResult.semanticResult?.code || "semantic timeout",
|
|
4786
|
+
envelope: dispatchResult.envelope,
|
|
4787
|
+
turnPrompt: dispatchResult.turnPrompt,
|
|
4788
|
+
};
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
return {
|
|
4792
|
+
ok: true,
|
|
4793
|
+
elapsedMs: Date.now() - startTime,
|
|
4794
|
+
envelope: dispatchResult.envelope,
|
|
4795
|
+
publishResult: dispatchResult.publishResult,
|
|
4796
|
+
transportResult: dispatchResult.transportResult,
|
|
4797
|
+
semanticResult: dispatchResult.semanticResult,
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
async function runUntilBlockedCore(sessionId, {
|
|
4802
|
+
maxTurns = 12,
|
|
4803
|
+
cliTimeoutSec = 120,
|
|
4804
|
+
browserTimeoutSec = 45,
|
|
4805
|
+
includeHistoryEntries = 4,
|
|
4806
|
+
} = {}) {
|
|
4807
|
+
const steps = [];
|
|
4808
|
+
|
|
4809
|
+
for (let iteration = 0; iteration < maxTurns; iteration += 1) {
|
|
4810
|
+
const state = loadSession(sessionId);
|
|
4811
|
+
if (!state) {
|
|
4812
|
+
return { ok: false, status: "missing", error: "Session not found", steps };
|
|
4813
|
+
}
|
|
4814
|
+
if (state.status !== "active" || state.current_speaker === "none") {
|
|
4815
|
+
return { ok: true, status: state.status, steps };
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
const speaker = state.current_speaker;
|
|
4819
|
+
const { transport } = resolveTransportForSpeaker(state, speaker);
|
|
4820
|
+
const callerSpeaker = detectCallerSpeaker();
|
|
4821
|
+
if (transport === "cli_respond" && callerSpeaker && normalizeSpeaker(callerSpeaker) === normalizeSpeaker(speaker)) {
|
|
4822
|
+
return {
|
|
4823
|
+
ok: true,
|
|
4824
|
+
status: "blocked",
|
|
4825
|
+
block_reason: "self_turn",
|
|
4826
|
+
speaker,
|
|
4827
|
+
transport,
|
|
4828
|
+
turn_prompt: buildClipboardTurnPrompt(state, speaker, null, includeHistoryEntries),
|
|
4829
|
+
steps,
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4833
|
+
if (transport === "manual" || transport === "clipboard") {
|
|
4834
|
+
return {
|
|
4835
|
+
ok: true,
|
|
4836
|
+
status: "blocked",
|
|
4837
|
+
block_reason: "manual_transport",
|
|
4838
|
+
speaker,
|
|
4839
|
+
transport,
|
|
4840
|
+
turn_prompt: buildClipboardTurnPrompt(state, speaker, null, includeHistoryEntries),
|
|
4841
|
+
steps,
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
let result = null;
|
|
4846
|
+
if (transport === "cli_respond") {
|
|
4847
|
+
result = await runCliAutoTurnCore(sessionId, speaker, cliTimeoutSec);
|
|
4848
|
+
} else if (transport === "browser_auto") {
|
|
4849
|
+
result = await runBrowserAutoTurnCore(sessionId, speaker, browserTimeoutSec);
|
|
4850
|
+
} else if (transport === "telepty_bus") {
|
|
4851
|
+
result = await runTeleptyBusAutoTurnCore(sessionId, speaker, includeHistoryEntries);
|
|
4852
|
+
} else {
|
|
4853
|
+
return {
|
|
4854
|
+
ok: true,
|
|
4855
|
+
status: "blocked",
|
|
4856
|
+
block_reason: "unsupported_transport",
|
|
4857
|
+
speaker,
|
|
4858
|
+
transport,
|
|
4859
|
+
turn_prompt: buildClipboardTurnPrompt(state, speaker, null, includeHistoryEntries),
|
|
4860
|
+
steps,
|
|
4861
|
+
};
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
steps.push({
|
|
4865
|
+
speaker,
|
|
4866
|
+
transport,
|
|
4867
|
+
ok: Boolean(result?.ok),
|
|
4868
|
+
error: result?.error || null,
|
|
4869
|
+
elapsedMs: result?.elapsedMs || null,
|
|
4870
|
+
blocked: Boolean(result?.blocked),
|
|
4871
|
+
});
|
|
4872
|
+
|
|
4873
|
+
if (!result?.ok) {
|
|
4874
|
+
return {
|
|
4875
|
+
ok: Boolean(result?.blocked),
|
|
4876
|
+
status: result?.blocked ? "blocked" : "error",
|
|
4877
|
+
block_reason: result?.blocked ? (result.error || "transport_blocked") : null,
|
|
4878
|
+
speaker,
|
|
4879
|
+
transport,
|
|
4880
|
+
error: result?.error || null,
|
|
4881
|
+
turn_prompt: result?.turnPrompt || null,
|
|
4882
|
+
steps,
|
|
4883
|
+
};
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
const finalState = loadSession(sessionId);
|
|
4888
|
+
return {
|
|
4889
|
+
ok: true,
|
|
4890
|
+
status: finalState?.status === "active" ? "max_turns_reached" : (finalState?.status || "completed"),
|
|
4891
|
+
steps,
|
|
4892
|
+
};
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4363
4895
|
/**
|
|
4364
4896
|
* Generate structured synthesis by calling a CLI speaker with a synthesis prompt.
|
|
4365
4897
|
*/
|
|
@@ -4481,27 +5013,14 @@ async function runAutoHandoff(sessionId) {
|
|
|
4481
5013
|
|
|
4482
5014
|
appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN: ${sessionId} | speaker: ${speaker} | round: ${state.current_round}/${state.max_rounds}`);
|
|
4483
5015
|
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
if (freshState) {
|
|
4490
|
-
// Advance to next speaker manually
|
|
4491
|
-
const idx = freshState.speakers.indexOf(speaker);
|
|
4492
|
-
const nextIdx = (idx + 1) % freshState.speakers.length;
|
|
4493
|
-
freshState.current_speaker = freshState.speakers[nextIdx];
|
|
4494
|
-
if (nextIdx === 0) freshState.current_round++;
|
|
4495
|
-
if (freshState.current_round > freshState.max_rounds) {
|
|
4496
|
-
freshState.status = "awaiting_synthesis";
|
|
4497
|
-
freshState.current_speaker = "none";
|
|
4498
|
-
}
|
|
4499
|
-
saveSession(freshState);
|
|
4500
|
-
}
|
|
4501
|
-
continue;
|
|
5016
|
+
const runResult = await runUntilBlockedCore(sessionId, { maxTurns: 1, includeHistoryEntries: 3 });
|
|
5017
|
+
const step = runResult.steps.at(-1) || null;
|
|
5018
|
+
if (!runResult.ok || runResult.status === "blocked") {
|
|
5019
|
+
appendRuntimeLog("WARN", `AUTO_HANDOFF_TURN_BLOCKED: ${sessionId} | speaker: ${speaker} | ${runResult.block_reason || runResult.error || "unknown"}`);
|
|
5020
|
+
break;
|
|
4502
5021
|
}
|
|
4503
5022
|
|
|
4504
|
-
appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN_OK: ${sessionId} | speaker: ${speaker} | ${
|
|
5023
|
+
appendRuntimeLog("INFO", `AUTO_HANDOFF_TURN_OK: ${sessionId} | speaker: ${speaker} | ${step?.elapsedMs || 0}ms`);
|
|
4505
5024
|
}
|
|
4506
5025
|
|
|
4507
5026
|
// Phase 2: Generate structured synthesis
|
|
@@ -4797,6 +5316,56 @@ server.tool(
|
|
|
4797
5316
|
})
|
|
4798
5317
|
);
|
|
4799
5318
|
|
|
5319
|
+
server.tool(
|
|
5320
|
+
"deliberation_run_until_blocked",
|
|
5321
|
+
"Auto-run a deliberation across mixed transports until it completes or reaches a manual/blocking turn.",
|
|
5322
|
+
{
|
|
5323
|
+
session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
|
|
5324
|
+
max_turns: z.number().int().min(1).max(50).default(12).describe("Maximum number of turns to auto-run before stopping"),
|
|
5325
|
+
cli_timeout_sec: z.number().int().min(30).max(900).default(120).describe("CLI auto-turn timeout (seconds)"),
|
|
5326
|
+
browser_timeout_sec: z.number().int().min(15).max(300).default(45).describe("Browser auto-turn timeout (seconds)"),
|
|
5327
|
+
include_history_entries: z.number().int().min(0).max(12).default(4).describe("Recent log entries to include for telepty turns"),
|
|
5328
|
+
},
|
|
5329
|
+
safeToolHandler("deliberation_run_until_blocked", async ({ session_id, max_turns, cli_timeout_sec, browser_timeout_sec, include_history_entries }) => {
|
|
5330
|
+
const resolved = resolveSessionId(session_id);
|
|
5331
|
+
if (!resolved) {
|
|
5332
|
+
return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
|
|
5333
|
+
}
|
|
5334
|
+
if (resolved === "MULTIPLE") {
|
|
5335
|
+
return { content: [{ type: "text", text: multipleSessionsError() }] };
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
const initialState = loadSession(resolved);
|
|
5339
|
+
if (!initialState || initialState.status !== "active") {
|
|
5340
|
+
return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
const result = await runUntilBlockedCore(resolved, {
|
|
5344
|
+
maxTurns: max_turns,
|
|
5345
|
+
cliTimeoutSec: cli_timeout_sec,
|
|
5346
|
+
browserTimeoutSec: browser_timeout_sec,
|
|
5347
|
+
includeHistoryEntries: include_history_entries,
|
|
5348
|
+
});
|
|
5349
|
+
const finalState = loadSession(resolved);
|
|
5350
|
+
const stepsText = (result.steps || []).length > 0
|
|
5351
|
+
? result.steps.map((step, index) => `- ${index + 1}. ${step.speaker} [${step.transport}] → ${step.ok ? "ok" : (step.blocked ? `blocked (${step.error || "blocked"})` : `error (${step.error || "unknown"})`)}${step.elapsedMs ? ` (${step.elapsedMs}ms)` : ""}`).join("\n")
|
|
5352
|
+
: "- none";
|
|
5353
|
+
|
|
5354
|
+
let summary = `## Run Until Blocked — ${resolved}\n\n`;
|
|
5355
|
+
summary += `**Result:** ${result.status}\n`;
|
|
5356
|
+
summary += `**Current state:** ${finalState?.status || initialState.status}\n`;
|
|
5357
|
+
summary += `**Current speaker:** ${finalState?.current_speaker || initialState.current_speaker}\n`;
|
|
5358
|
+
if (result.block_reason) summary += `**Block reason:** ${result.block_reason}\n`;
|
|
5359
|
+
if (result.error) summary += `**Error:** ${result.error}\n`;
|
|
5360
|
+
if (result.turn_prompt) {
|
|
5361
|
+
summary += `\n### [turn_prompt]\n\`\`\`markdown\n${result.turn_prompt}\n\`\`\`\n`;
|
|
5362
|
+
}
|
|
5363
|
+
summary += `\n### Steps\n${stepsText}\n`;
|
|
5364
|
+
|
|
5365
|
+
return { content: [{ type: "text", text: summary }] };
|
|
5366
|
+
})
|
|
5367
|
+
);
|
|
5368
|
+
|
|
4800
5369
|
server.tool(
|
|
4801
5370
|
"deliberation_respond",
|
|
4802
5371
|
"Submit a response for the current turn.",
|
|
@@ -4880,6 +5449,51 @@ server.tool(
|
|
|
4880
5449
|
})
|
|
4881
5450
|
);
|
|
4882
5451
|
|
|
5452
|
+
server.tool(
|
|
5453
|
+
"deliberation_ingest_remote_reply",
|
|
5454
|
+
"Canonical semantic ingress for replies produced on another machine/session. Use this instead of reconstructing deliberation state from transport events.",
|
|
5455
|
+
{
|
|
5456
|
+
session_id: z.string().describe("Deliberation session ID"),
|
|
5457
|
+
speaker: z.string().describe("Speaker name"),
|
|
5458
|
+
turn_id: z.string().min(1).describe("Turn ID associated with the issued turn_request"),
|
|
5459
|
+
content: z.string().min(1).describe("Remote reply content"),
|
|
5460
|
+
source_machine_id: z.string().min(1).describe("Source machine or peer identifier"),
|
|
5461
|
+
source_session_id: z.string().min(1).describe("Source remote session identifier"),
|
|
5462
|
+
transport_scope: z.string().min(1).describe("Transport scope used to carry the remote reply"),
|
|
5463
|
+
artifact_refs: z.array(z.string().min(1)).optional().describe("Optional artifact references the reply depends on"),
|
|
5464
|
+
reply_origin: z.string().optional().describe("Optional origin hint, e.g. remote_mcp, telepty_thread"),
|
|
5465
|
+
timestamp: z.string().optional().describe("Optional source timestamp"),
|
|
5466
|
+
},
|
|
5467
|
+
safeToolHandler("deliberation_ingest_remote_reply", async ({
|
|
5468
|
+
session_id,
|
|
5469
|
+
speaker,
|
|
5470
|
+
turn_id,
|
|
5471
|
+
content,
|
|
5472
|
+
source_machine_id,
|
|
5473
|
+
source_session_id,
|
|
5474
|
+
transport_scope,
|
|
5475
|
+
artifact_refs,
|
|
5476
|
+
reply_origin,
|
|
5477
|
+
timestamp,
|
|
5478
|
+
}) => {
|
|
5479
|
+
return submitDeliberationTurn({
|
|
5480
|
+
session_id,
|
|
5481
|
+
speaker,
|
|
5482
|
+
content,
|
|
5483
|
+
turn_id,
|
|
5484
|
+
channel_used: `remote_ingress:${transport_scope}`,
|
|
5485
|
+
source_metadata: {
|
|
5486
|
+
source_machine_id,
|
|
5487
|
+
source_session_id,
|
|
5488
|
+
transport_scope,
|
|
5489
|
+
artifact_refs: artifact_refs || [],
|
|
5490
|
+
reply_origin: reply_origin || null,
|
|
5491
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
5492
|
+
},
|
|
5493
|
+
});
|
|
5494
|
+
})
|
|
5495
|
+
);
|
|
5496
|
+
|
|
4883
5497
|
server.tool(
|
|
4884
5498
|
"deliberation_list_remote_sessions",
|
|
4885
5499
|
"List all active deliberation sessions on a remote machine (via Tailscale/IP) to find the correct session_id for context injection.",
|
|
@@ -5118,6 +5732,9 @@ server.tool(
|
|
|
5118
5732
|
notifyTeleptyBus(synthesisEnvelope).catch(() => {}); // fire-and-forget
|
|
5119
5733
|
}
|
|
5120
5734
|
|
|
5735
|
+
// Notify brain ingest if endpoint configured
|
|
5736
|
+
callBrainIngest(state.execution_contract).catch(() => {}); // fire-and-forget
|
|
5737
|
+
|
|
5121
5738
|
return {
|
|
5122
5739
|
content: [{
|
|
5123
5740
|
type: "text",
|
|
@@ -6102,4 +6719,4 @@ if (__entryFile && path.resolve(__currentFile) === __entryFile) {
|
|
|
6102
6719
|
}
|
|
6103
6720
|
|
|
6104
6721
|
// ── Test exports (used by vitest) ──
|
|
6105
|
-
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, hasExplicitBrowserParticipantSelection, resolveIncludeBrowserSpeakers, confirmSpeakerSelectionToken, validateSpeakerSelectionRequest, truncatePromptText, getPromptBudgetForSpeaker, formatRecentLogForPrompt, getCliAutoTurnTimeoutSec, getCliExecArgs, buildCliAutoTurnFailureText, buildClipboardTurnPrompt, getProjectStateDir, loadSession, saveSession, listActiveSessions, multipleSessionsError, findSessionRecord, mapParticipantProfiles, formatSpeakerCandidatesReport, buildTeleptyTurnRequestEnvelope, buildTeleptySynthesisEnvelope, validateTeleptyEnvelope, registerPendingTeleptyTurnRequest, handleTeleptyBusMessage, completePendingTeleptySemantic, cleanupPendingTeleptyTurn, getTeleptySessionHealth, TELEPTY_TRANSPORT_TIMEOUT_MS, TELEPTY_SEMANTIC_TIMEOUT_MS };
|
|
6722
|
+
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, hasExplicitBrowserParticipantSelection, resolveIncludeBrowserSpeakers, confirmSpeakerSelectionToken, validateSpeakerSelectionRequest, truncatePromptText, getPromptBudgetForSpeaker, formatRecentLogForPrompt, getCliAutoTurnTimeoutSec, getCliExecArgs, buildCliAutoTurnFailureText, buildClipboardTurnPrompt, getProjectStateDir, loadSession, saveSession, listActiveSessions, multipleSessionsError, findSessionRecord, mapParticipantProfiles, formatSpeakerCandidatesReport, buildTeleptyTurnRequestEnvelope, buildTeleptyTurnCompletedEnvelope, buildTeleptySynthesisEnvelope, validateTeleptyEnvelope, registerPendingTeleptyTurnRequest, handleTeleptyBusMessage, completePendingTeleptySemantic, cleanupPendingTeleptyTurn, getTeleptySessionHealth, TELEPTY_TRANSPORT_TIMEOUT_MS, TELEPTY_SEMANTIC_TIMEOUT_MS };
|
package/install.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* What it does:
|
|
11
11
|
* 1. Copies server files to ~/.local/lib/mcp-deliberation/
|
|
12
12
|
* 2. Installs npm dependencies
|
|
13
|
-
* 3. Registers MCP server
|
|
13
|
+
* 3. Registers MCP server via `claude mcp add` (Claude Code)
|
|
14
14
|
* 4. Registers MCP server in ~/.gemini/settings.json (Gemini CLI)
|
|
15
15
|
* 5. Ready to use — next Claude Code or Gemini CLI session will auto-load
|
|
16
16
|
* 6. Installs skill file (~/.claude/skills/deliberation-gate/SKILL.md)
|
|
@@ -61,6 +61,15 @@ function log(msg) {
|
|
|
61
61
|
console.log(` ${msg}`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function commandExists(cmd) {
|
|
65
|
+
try {
|
|
66
|
+
execSync(IS_WIN ? `where ${cmd}` : `command -v ${cmd}`, { stdio: "pipe" });
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
function copyFileIfExists(src, dest) {
|
|
65
74
|
if (fs.existsSync(src)) {
|
|
66
75
|
fs.copyFileSync(src, dest);
|
|
@@ -121,32 +130,55 @@ function install() {
|
|
|
121
130
|
log(" Manual fix: cd ~/.local/lib/mcp-deliberation && npm install");
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
// Step 4: Register MCP server
|
|
133
|
+
// Step 4: Register MCP server (Claude Code)
|
|
125
134
|
log("🔧 Registering Claude Code MCP server...");
|
|
126
|
-
const
|
|
127
|
-
|
|
135
|
+
const serverEntryPoint = toForwardSlash(path.join(INSTALL_DIR, "index.js"));
|
|
136
|
+
let claudeRegistered = false;
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
if (
|
|
138
|
+
// Prefer `claude mcp add` — this is the only method Claude Code reliably reads
|
|
139
|
+
if (commandExists("claude")) {
|
|
131
140
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
// Remove existing registration first (ignore errors if not registered)
|
|
142
|
+
try { execSync("claude mcp remove deliberation -s user", { stdio: "pipe" }); } catch { /* ok */ }
|
|
143
|
+
execSync(`claude mcp add deliberation -s user -- node "${serverEntryPoint}"`, {
|
|
144
|
+
stdio: "pipe",
|
|
145
|
+
});
|
|
146
|
+
log(" → Registered via 'claude mcp add' (user scope)");
|
|
147
|
+
claudeRegistered = true;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
log(` ⚠️ 'claude mcp add' failed: ${err.message}`);
|
|
135
150
|
}
|
|
136
151
|
}
|
|
137
152
|
|
|
138
|
-
|
|
153
|
+
// Fallback: write ~/.claude/.mcp.json (legacy, may not be read by Claude Code)
|
|
154
|
+
if (!claudeRegistered) {
|
|
155
|
+
const claudeDir = path.join(HOME, ".claude");
|
|
156
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
let mcpConfig = {};
|
|
159
|
+
if (fs.existsSync(MCP_CONFIG)) {
|
|
160
|
+
try {
|
|
161
|
+
mcpConfig = JSON.parse(fs.readFileSync(MCP_CONFIG, "utf-8"));
|
|
162
|
+
} catch {
|
|
163
|
+
mcpConfig = {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
168
|
+
|
|
169
|
+
const alreadyRegistered = !!mcpConfig.mcpServers.deliberation;
|
|
170
|
+
mcpConfig.mcpServers.deliberation = {
|
|
171
|
+
command: "node",
|
|
172
|
+
args: [serverEntryPoint],
|
|
173
|
+
};
|
|
145
174
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
175
|
+
fs.writeFileSync(MCP_CONFIG, JSON.stringify(mcpConfig, null, 2));
|
|
176
|
+
log(alreadyRegistered
|
|
177
|
+
? " → Fallback: existing registration updated in .mcp.json"
|
|
178
|
+
: " → Fallback: registered in .mcp.json");
|
|
179
|
+
log(" ⚠️ Claude CLI not found. Run manually if server isn't detected:");
|
|
180
|
+
log(` claude mcp add deliberation -s user -- node "${serverEntryPoint}"`);
|
|
181
|
+
}
|
|
150
182
|
|
|
151
183
|
// Step 5: Register Gemini CLI MCP server
|
|
152
184
|
log("🔧 Registering Gemini CLI MCP server...");
|
|
@@ -283,14 +315,24 @@ Skill path: ${SKILL_DEST}
|
|
|
283
315
|
} else if (args.includes("--uninstall") || args.includes("uninstall")) {
|
|
284
316
|
console.log("\n🗑️ Deliberation MCP Server — Uninstalling\n");
|
|
285
317
|
|
|
286
|
-
// Remove from Claude MCP
|
|
318
|
+
// Remove from Claude Code MCP registration
|
|
319
|
+
let claudeUnregistered = false;
|
|
320
|
+
if (commandExists("claude")) {
|
|
321
|
+
try {
|
|
322
|
+
execSync("claude mcp remove deliberation -s user", { stdio: "pipe" });
|
|
323
|
+
log("Claude Code MCP server unregistered via 'claude mcp remove'");
|
|
324
|
+
claudeUnregistered = true;
|
|
325
|
+
} catch { /* may not exist */ }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Also clean up legacy .mcp.json fallback
|
|
287
329
|
if (fs.existsSync(MCP_CONFIG)) {
|
|
288
330
|
try {
|
|
289
331
|
const mcpConfig = JSON.parse(fs.readFileSync(MCP_CONFIG, "utf-8"));
|
|
290
332
|
if (mcpConfig.mcpServers?.deliberation) {
|
|
291
333
|
delete mcpConfig.mcpServers.deliberation;
|
|
292
334
|
fs.writeFileSync(MCP_CONFIG, JSON.stringify(mcpConfig, null, 2));
|
|
293
|
-
log("Claude Code MCP server unregistered");
|
|
335
|
+
if (!claudeUnregistered) log("Claude Code MCP server unregistered from .mcp.json");
|
|
294
336
|
}
|
|
295
337
|
} catch { /* ignore */ }
|
|
296
338
|
}
|
package/package.json
CHANGED
|
@@ -31,7 +31,9 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
|
|
|
31
31
|
| `deliberation_context` | 프로젝트 컨텍스트 로드 | 불필요 |
|
|
32
32
|
| `deliberation_browser_llm_tabs` | 브라우저 LLM 탭 목록 (웹 기반 LLM 참여용) | 불필요 |
|
|
33
33
|
| `deliberation_route_turn` | 현재 차례 speaker의 transport(CLI/browser_auto/manual)를 자동 라우팅 | 선택적* |
|
|
34
|
+
| `deliberation_run_until_blocked` | CLI/browser_auto/telepty_bus를 자동 진행하다 막히는 지점에서 중단 | 선택적 |
|
|
34
35
|
| `deliberation_respond` | 현재 차례의 응답 제출 | 선택적* |
|
|
36
|
+
| `deliberation_ingest_remote_reply` | 원격 머신 reply를 명시적 source metadata와 함께 semantic ingest | 선택적 |
|
|
35
37
|
| `deliberation_history` | 전체 토론 기록 조회 | 선택적* |
|
|
36
38
|
| `deliberation_synthesize` | 합성 보고서 생성 및 토론 완료 | 선택적* |
|
|
37
39
|
| `deliberation_list` | 과거 토론 아카이브 목록 | 불필요 |
|
|
@@ -64,9 +66,11 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
|
|
|
64
66
|
- CLI speaker → `deliberation_cli_auto_turn`으로 실제 CLI 실행
|
|
65
67
|
- browser_auto → CDP로 자동 전송/수집
|
|
66
68
|
- clipboard/manual → 클립보드 준비 + 사용자 안내
|
|
69
|
+
- 완전 자동으로 여러 턴을 밀고 싶으면 `deliberation_run_until_blocked`를 사용합니다. 이 도구는 `cli_respond`, `browser_auto`, `telepty_bus`를 연속 실행하고, 수동 transport 또는 self-turn에서 멈춥니다.
|
|
67
70
|
3. **NEVER** — 오케스트레이터(자기 자신)가 다른 speaker를 대신하여 `deliberation_respond`에 응답을 작성하지 마세요. 이것은 "역할극"이며 실제 deliberation이 아닙니다. MCP 서버가 이를 감지하고 차단합니다.
|
|
68
71
|
4. **NEVER** — `deliberation_respond`를 직접 호출하지 마세요 (자기 자신의 응답 제외). 다른 speaker의 턴은 반드시 `deliberation_route_turn` 또는 `deliberation_cli_auto_turn`/`deliberation_browser_auto_turn`을 통해 진행합니다.
|
|
69
72
|
5. **MUST** — 자기 자신(오케스트레이터 역할의 claude)이 speaker인 경우에만 직접 `deliberation_respond`로 응답을 제출할 수 있습니다.
|
|
73
|
+
6. **MUST** — 원격 머신/세션에서 들어온 응답을 semantic reply로 반영할 때는 raw bus event를 해석하지 말고 `deliberation_ingest_remote_reply`를 사용하세요. transport trace와 semantic ingest를 섞지 마세요.
|
|
70
74
|
|
|
71
75
|
## 워크플로우
|
|
72
76
|
|
|
@@ -95,7 +99,9 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
|
|
|
95
99
|
5. **`deliberation_route_turn` 호출 (필수)** → 현재 차례 speaker transport 자동 결정 및 실행
|
|
96
100
|
- CLI speaker → `deliberation_cli_auto_turn`이 실제 CLI를 실행하고 응답 수집
|
|
97
101
|
- browser_auto → CDP로 자동 전송/수집
|
|
102
|
+
- telepty_bus → structured `turn_request` publish + remote self-submit 대기
|
|
98
103
|
- 자기 자신(claude)이 speaker → 직접 `deliberation_respond`로 응답 제출
|
|
104
|
+
- 여러 턴을 한 번에 진행하려면 `deliberation_run_until_blocked(session_id)` 사용
|
|
99
105
|
6. 반복 후 `deliberation_synthesize(session_id)` → 합성 완료
|
|
100
106
|
7. 구현이 필요하면 `deliberation-executor` 스킬로 handoff
|
|
101
107
|
예: "session_id {id} 합의안 구현해줘"
|
|
@@ -157,6 +163,20 @@ autoresearch 스타일 실험 루프를 검토할 때는 긴 컨텍스트를 `to
|
|
|
157
163
|
})
|
|
158
164
|
```
|
|
159
165
|
|
|
166
|
+
원격 reply를 semantic turn으로 넣어야 할 때:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
deliberation_ingest_remote_reply(
|
|
170
|
+
session_id: "<session_id>",
|
|
171
|
+
speaker: "<speaker>",
|
|
172
|
+
turn_id: "<pending_turn_id>",
|
|
173
|
+
content: "<reply markdown>",
|
|
174
|
+
source_machine_id: "peer-01",
|
|
175
|
+
source_session_id: "remote-gemini-001",
|
|
176
|
+
transport_scope: "remote_mcp"
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
160
180
|
### D. 자동 진행 (스크립트)
|
|
161
181
|
```bash
|
|
162
182
|
# 새 토론
|
|
@@ -233,3 +253,36 @@ bash deliberation-monitor.sh --tmux
|
|
|
233
253
|
|
|
234
254
|
- **deliberation-gate**: superpowers 워크플로우 통합 스킬. brainstorming/code-review/debugging 의사결정 지점에 멀티-AI 검증 게이트를 삽입합니다. `~/.claude/skills/deliberation-gate/SKILL.md`에 설치.
|
|
235
255
|
- **deliberation-executor**: deliberation 합의안을 실제 코드 구현으로 전환하는 실행 전용 스킬.
|
|
256
|
+
|
|
257
|
+
## Data Model: Canonical Roles
|
|
258
|
+
|
|
259
|
+
Deliberation produces two complementary data artifacts after synthesis:
|
|
260
|
+
|
|
261
|
+
| Artifact | Role | Consumers |
|
|
262
|
+
|----------|------|-----------|
|
|
263
|
+
| `structured_synthesis` | **Human + reasoning canonical** | Human reviewers, LLM context, decision history |
|
|
264
|
+
| `execution_contract` | **Automation canonical** | inbox-watcher, devkit, registry, orchestrator agents |
|
|
265
|
+
|
|
266
|
+
### structured_synthesis (Human Canonical)
|
|
267
|
+
Rich context for human review. Contains:
|
|
268
|
+
- `summary`: natural language overview
|
|
269
|
+
- `decisions`: reasoning and rationale
|
|
270
|
+
- `actionable_tasks`: full task descriptions with context
|
|
271
|
+
- `experiment_outcome`: optional verdict (keep/discard/modify)
|
|
272
|
+
|
|
273
|
+
### execution_contract (Automation Canonical)
|
|
274
|
+
Minimal, deterministic task list for machines. Contains:
|
|
275
|
+
- `version`: contract schema version (currently "v1")
|
|
276
|
+
- `source_session_id`: originating deliberation session
|
|
277
|
+
- `summary`: brief summary for log context
|
|
278
|
+
- `tasks`: flattened actionable task list (same shape as `actionable_tasks`)
|
|
279
|
+
- `generated_from.structured_synthesis_hash`: SHA-1 provenance hash
|
|
280
|
+
|
|
281
|
+
**Rule**: Automation consumers MUST prefer `execution_contract` when present.
|
|
282
|
+
Fall back to `structured_synthesis` only when `execution_contract` is `null`.
|
|
283
|
+
|
|
284
|
+
### Archive Outputs
|
|
285
|
+
When a deliberation completes:
|
|
286
|
+
1. **Markdown archive**: `~/.local/lib/mcp-deliberation/state/{project}/archive/deliberation-{ts}-{slug}.md`
|
|
287
|
+
2. **Contract sidecar**: `~/.local/lib/mcp-deliberation/state/{project}/archive/deliberation-{ts}-{slug}.contract.json`
|
|
288
|
+
3. **Telepty envelope**: `deliberation_completed` event with both artifacts in payload
|