@bolloon/bolloon-agent 0.2.0 → 0.2.2

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.
Files changed (42) hide show
  1. package/dist/agents/judgment-protocol.js +479 -0
  2. package/dist/agents/pi-sdk.js +269 -1
  3. package/dist/electron/config.js +9 -14
  4. package/dist/electron/dialogs.js +16 -53
  5. package/dist/electron/first-run.js +24 -65
  6. package/dist/electron/ipc.js +10 -14
  7. package/dist/electron/logger.js +7 -44
  8. package/dist/electron/main.js +38 -41
  9. package/dist/electron/menu.js +13 -18
  10. package/dist/electron/paths.js +12 -54
  11. package/dist/electron/server.js +18 -56
  12. package/dist/electron/tray.js +15 -53
  13. package/dist/electron/window.js +22 -61
  14. package/dist/security/tool-gate.js +4 -0
  15. package/dist/web/client.js +3767 -2873
  16. package/dist/web/components/p2p/P2PModal.js +188 -0
  17. package/dist/web/components/p2p/index.js +264 -226
  18. package/dist/web/components/p2p/p2p-modal.js +657 -0
  19. package/dist/web/components/p2p/p2p-tools.js +248 -0
  20. package/dist/web/ui/message-renderer.js +442 -326
  21. package/dist/web/ui/step-timeline.js +351 -255
  22. package/ios/App/App/AppDelegate.swift +49 -0
  23. package/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png +0 -0
  24. package/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  25. package/ios/App/App/Assets.xcassets/Contents.json +6 -0
  26. package/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json +23 -0
  27. package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png +0 -0
  28. package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png +0 -0
  29. package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png +0 -0
  30. package/ios/App/App/Base.lproj/LaunchScreen.storyboard +32 -0
  31. package/ios/App/App/Base.lproj/Main.storyboard +19 -0
  32. package/ios/App/App/Info.plist +88 -0
  33. package/ios/App/App.xcodeproj/project.pbxproj +376 -0
  34. package/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  35. package/ios/App/CapApp-SPM/Package.swift +25 -0
  36. package/ios/App/CapApp-SPM/README.md +5 -0
  37. package/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift +1 -0
  38. package/ios/debug.xcconfig +1 -0
  39. package/package.json +14 -3
  40. package/scripts/build-app-bundle.cjs +170 -0
  41. package/.comm/README.md +0 -21
  42. package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +0 -7
@@ -0,0 +1,479 @@
1
+ /**
2
+ * judgment-protocol — bolloon 的 4 个核心协议消息
3
+ *
4
+ * 协议定义见 docs/真正要做的事.md §3.2:
5
+ * - ask (A→B): 就决策 X 发起 3 个反方蒸馏
6
+ * - dissent (A↔B): 双方反方观点互见
7
+ * - align (A↔B): 基于反方互见形成对齐结论
8
+ * - reflect (本地): 整链闭环后沉淀成 judgment
9
+ *
10
+ * Transport: iroh (复用现有 IrohTransport, 不另起).
11
+ * 4 个消息 kind: 'judgment_ask' | 'judgment_dissent' | 'judgment_align'
12
+ * reflect 不上链, 直接落 ~/.bolloon/judgments/{askId}.yaml
13
+ *
14
+ * 设计取舍:
15
+ * - 协议消息是 askId-keyed 的 (一个决策一条链)
16
+ * - dissent 至少 3 个反方 (协议硬约束, 协议四要素里"反方互见"的最小颗粒)
17
+ * - dissent 不要求显式命令 — 收到 ask 自动蒸馏本方 3 反方回发
18
+ * - align 必须引用 askId + at least 1 dissentId per side, 否则视为未基于"互见"
19
+ * - reflect 是本地, 写盘后 emit 一个 'reflected' 事件, UI 可订阅
20
+ *
21
+ * 蒸馏 (distillDissent) 调用现有 human-value-store 的判断力:
22
+ * loadAllJudgments + getValueProfile, 用 LLM 生成 3 个反方.
23
+ * LLM 不可用时降级到启发式 (从历史 judgment 抽最近 3 条 reasons 当反方骨架).
24
+ */
25
+ import * as crypto from 'crypto';
26
+ import * as fs from 'fs/promises';
27
+ import * as path from 'path';
28
+ import * as os from 'os';
29
+ import { EventEmitter } from 'events';
30
+ import { irohTransport as defaultIrohTransport } from '../network/iroh-transport.js';
31
+ import { loadAllJudgments, getValueProfile, storeHumanJudgment, getRelevantValues } from '../pi-ecosystem-judgment/human-value-store.js';
32
+ class JudgmentEventBus extends EventEmitter {
33
+ }
34
+ export const judgmentEventBus = new JudgmentEventBus();
35
+ // ============================================================================
36
+ // 持久化 (本地 chain registry)
37
+ // ~/.bolloon/judgments/chains/{askId}.json
38
+ // ~/.bolloon/judgments/yaml/{askId}.yaml (reflect 后写出)
39
+ // ============================================================================
40
+ function homeDir() {
41
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
42
+ }
43
+ function chainsDir() {
44
+ return path.join(homeDir(), '.bolloon', 'judgments', 'chains');
45
+ }
46
+ function yamlDir() {
47
+ return path.join(homeDir(), '.bolloon', 'judgments', 'yaml');
48
+ }
49
+ function chainPath(askId) {
50
+ return path.join(chainsDir(), `${askId}.json`);
51
+ }
52
+ async function ensureDirs() {
53
+ await fs.mkdir(chainsDir(), { recursive: true });
54
+ await fs.mkdir(yamlDir(), { recursive: true });
55
+ }
56
+ async function readChain(askId) {
57
+ try {
58
+ const buf = await fs.readFile(chainPath(askId), 'utf-8');
59
+ return JSON.parse(buf);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ async function writeChain(chain) {
66
+ await ensureDirs();
67
+ await fs.writeFile(chainPath(chain.ask.askId), JSON.stringify(chain, null, 2), 'utf-8');
68
+ }
69
+ // ============================================================================
70
+ // 帧构造 / 解析
71
+ // ============================================================================
72
+ function encodeAskFrame(ask) {
73
+ return new TextEncoder().encode(JSON.stringify({ kind: 'judgment_ask', payload: ask, ts: ask.ts }));
74
+ }
75
+ function encodeDissentFrame(d) {
76
+ return new TextEncoder().encode(JSON.stringify({ kind: 'judgment_dissent', payload: d, ts: d.ts }));
77
+ }
78
+ function encodeAlignFrame(a) {
79
+ return new TextEncoder().encode(JSON.stringify({ kind: 'judgment_align', payload: a, ts: a.ts }));
80
+ }
81
+ function decodeFrame(buf) {
82
+ try {
83
+ const obj = JSON.parse(new TextDecoder().decode(buf));
84
+ if (typeof obj?.kind === 'string' && obj.payload)
85
+ return obj;
86
+ return null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ const states = new WeakMap();
93
+ function getState(transport) {
94
+ let s = states.get(transport);
95
+ if (!s) {
96
+ s = { listenersInstalled: false };
97
+ states.set(transport, s);
98
+ }
99
+ return s;
100
+ }
101
+ function ensureListeners(transport) {
102
+ const s = getState(transport);
103
+ if (s.listenersInstalled)
104
+ return;
105
+ s.listenersInstalled = true;
106
+ transport.onMessage('judgment_ask', async (msg) => {
107
+ const frame = decodeFrame(msg.payload);
108
+ if (!frame)
109
+ return;
110
+ const ask = frame.payload;
111
+ await onAskReceived(transport, ask);
112
+ });
113
+ transport.onMessage('judgment_dissent', async (msg) => {
114
+ const frame = decodeFrame(msg.payload);
115
+ if (!frame)
116
+ return;
117
+ const dissent = frame.payload;
118
+ await onDissentReceived(transport, dissent);
119
+ });
120
+ transport.onMessage('judgment_align', async (msg) => {
121
+ const frame = decodeFrame(msg.payload);
122
+ if (!frame)
123
+ return;
124
+ const align = frame.payload;
125
+ await onAlignReceived(transport, align);
126
+ });
127
+ }
128
+ // ============================================================================
129
+ // 蒸馏 (dissent 生成) — 协议核心载荷
130
+ // ============================================================================
131
+ /**
132
+ * 基于本方判断力 + ask 内容, 蒸馏 3 个反方观点.
133
+ * 协议硬约束: 至少 3 个, 最多 5 个.
134
+ */
135
+ export async function distillDissent(ask) {
136
+ const allJudgments = await loadAllJudgments().catch(() => []);
137
+ const valueProfile = await getValueProfile('me').catch(() => null);
138
+ const relevant = await getRelevantValues(ask.decision, undefined).catch(() => []);
139
+ const valueHint = valueProfile ? [
140
+ `quality_focus=${valueProfile.quality_focus.toFixed(2)}`,
141
+ `efficiency_focus=${valueProfile.efficiency_focus.toFixed(2)}`,
142
+ `safety_focus=${valueProfile.safety_focus.toFixed(2)}`,
143
+ `collaboration_focus=${valueProfile.collaboration_focus.toFixed(2)}`,
144
+ `learning_focus=${valueProfile.learning_focus.toFixed(2)}`,
145
+ ].join(', ') : '';
146
+ const recentReasons = allJudgments
147
+ .filter((j) => j.reasons?.length > 0)
148
+ .slice(-10)
149
+ .flatMap((j) => j.reasons || [])
150
+ .slice(0, 20);
151
+ const prompt = `你是用户的判断力代理. 用户发起了一个决定: "${ask.decision}"
152
+ ${ask.context ? `\n补充: ${ask.context}` : ''}
153
+
154
+ [用户价值画像] ${valueHint}
155
+ [相关历史判断 (${relevant.length} 条)] ${relevant.slice(0, 3).map((j) => j.decision || '').join(' | ').slice(0, 300)}
156
+ [历史理由样本] ${recentReasons.join(' | ').slice(0, 400) || '(无)'}
157
+
158
+ 请基于上述判断力, 生成正好 3 个"反方观点" (用户应该考虑但可能忽略的风险/盲点).
159
+ 要求:
160
+ - 每个反方 1-2 句, 总长 < 80 字
161
+ - 必须是这个特定决定的"反对声音", 不是通用建议
162
+ - 第 1 个: 历史/经验类风险
163
+ - 第 2 个: 关系/信任类风险
164
+ - 第 3 个: 隐藏代价/时间类风险
165
+ - 用 JSON 数组返回: ["反方1", "反方2", "反方3"]`;
166
+ // LLM 调用 (OpenAI 兼容, 复用 p2p-chat-tools 的方式)
167
+ let dissents = [];
168
+ try {
169
+ const openaiKey = process.env.OPENAI_API_KEY;
170
+ if (!openaiKey)
171
+ throw new Error('no LLM key');
172
+ const openaiBase = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
173
+ const openaiModel = process.env.OPENAI_MODEL || 'gpt-4';
174
+ const r = await fetch(`${openaiBase}/chat/completions`, {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${openaiKey}` },
177
+ body: JSON.stringify({
178
+ model: openaiModel,
179
+ messages: [
180
+ { role: 'system', content: '你只输出 JSON 数组, 不要解释. 必须正好 3 个元素.' },
181
+ { role: 'user', content: prompt },
182
+ ],
183
+ temperature: 0.7,
184
+ max_tokens: 400,
185
+ }),
186
+ signal: AbortSignal.timeout(20000),
187
+ });
188
+ if (r.ok) {
189
+ const data = await r.json();
190
+ const text = (data.choices?.[0]?.message?.content || '').trim();
191
+ // 尝试提取 JSON 数组
192
+ const match = text.match(/\[[\s\S]*?\]/);
193
+ if (match) {
194
+ const parsed = JSON.parse(match[0]);
195
+ if (Array.isArray(parsed))
196
+ dissents = parsed.filter((s) => typeof s === 'string' && s.trim()).slice(0, 5);
197
+ }
198
+ }
199
+ }
200
+ catch (e) {
201
+ console.warn('[judgment-protocol] LLM distill failed:', e.message);
202
+ }
203
+ // Fallback: 启发式 — 从历史 judgment 抽 3 条理由做骨架
204
+ if (dissents.length < 3) {
205
+ const fallback = recentReasons.slice(0, 5);
206
+ while (dissents.length < 3 && fallback.length > 0) {
207
+ dissents.push(`[启发式] ${fallback.shift()}`);
208
+ }
209
+ }
210
+ while (dissents.length < 3) {
211
+ dissents.push(`[占位反方 ${dissents.length + 1}] 这个决定可能忽略的盲点 (LLM 不可用, 请人工补充)`);
212
+ }
213
+ return dissents.slice(0, 5);
214
+ }
215
+ // ============================================================================
216
+ // 协议 handler — 收到对方消息时
217
+ // ============================================================================
218
+ async function onAskReceived(transport, ask) {
219
+ console.log(`[judgment-protocol] ask 收到: "${ask.decision.slice(0, 40)}..." from ${ask.proposerNodeId.slice(0, 12)}`);
220
+ // 1) 落本地 chain (init open)
221
+ let chain = await readChain(ask.askId);
222
+ if (!chain) {
223
+ chain = { ask, dissents: [], aligns: [], status: 'open', createdAt: Date.now() };
224
+ await writeChain(chain);
225
+ }
226
+ judgmentEventBus.emit('judgment', { kind: 'ask_received', askId: ask.askId, fromNodeId: ask.proposerNodeId });
227
+ // 2) 自动蒸馏我方 3 反方, 作为 dissent 回发 (协议: ask 必触发 dissent)
228
+ const dissents = await distillDissent(ask);
229
+ const myNodeId = transport.getNodeId() || 'unknown';
230
+ const dissent = {
231
+ dissentId: crypto.randomUUID(),
232
+ askId: ask.askId,
233
+ fromNodeId: myNodeId,
234
+ dissents,
235
+ ts: Date.now(),
236
+ };
237
+ await sendDissentInternal(transport, ask.proposerNodeId, dissent);
238
+ }
239
+ async function onDissentReceived(transport, dissent) {
240
+ console.log(`[judgment-protocol] dissent 收到: ${dissent.dissents.length} 反方 for ask ${dissent.askId.slice(0, 8)} from ${dissent.fromNodeId.slice(0, 12)}`);
241
+ const chain = await readChain(dissent.askId);
242
+ if (!chain) {
243
+ console.warn(`[judgment-protocol] 收到 dissent 但找不到对应 ask ${dissent.askId}, 跳过`);
244
+ return;
245
+ }
246
+ // 去重 (同一节点可能重发)
247
+ if (!chain.dissents.some((d) => d.dissentId === dissent.dissentId)) {
248
+ chain.dissents.push(dissent);
249
+ await writeChain(chain);
250
+ }
251
+ judgmentEventBus.emit('judgment', { kind: 'dissent_received', dissentId: dissent.dissentId, askId: dissent.askId, fromNodeId: dissent.fromNodeId });
252
+ }
253
+ async function onAlignReceived(transport, align) {
254
+ console.log(`[judgment-protocol] align 收到: "${align.conclusion.slice(0, 40)}..." for ask ${align.askId.slice(0, 8)}`);
255
+ const chain = await readChain(align.askId);
256
+ if (!chain) {
257
+ console.warn(`[judgment-protocol] 收到 align 但找不到对应 ask ${align.askId}, 跳过`);
258
+ return;
259
+ }
260
+ if (!chain.aligns.some((a) => a.alignId === align.alignId)) {
261
+ chain.aligns.push(align);
262
+ chain.status = 'aligned';
263
+ chain.closedAt = Date.now();
264
+ await writeChain(chain);
265
+ }
266
+ judgmentEventBus.emit('judgment', { kind: 'align_received', alignId: align.alignId, askId: align.askId, fromNodeId: align.fromNodeId });
267
+ }
268
+ // ============================================================================
269
+ // 公共 API — 调用方 (CLI / Web / Skill) 用
270
+ // ============================================================================
271
+ export async function initJudgmentProtocol(transport = defaultIrohTransport) {
272
+ ensureListeners(transport);
273
+ await ensureDirs();
274
+ console.log('[judgment-protocol] initialized, chains dir =', chainsDir());
275
+ }
276
+ export async function sendAsk(peerDID, decision, opts = {}, transport = defaultIrohTransport) {
277
+ ensureListeners(transport);
278
+ const myNodeId = transport.getNodeId() || 'unknown';
279
+ const ask = {
280
+ askId: crypto.randomUUID(),
281
+ decision,
282
+ proposerNodeId: myNodeId,
283
+ proposerName: opts.proposerName,
284
+ context: opts.context?.slice(0, 500),
285
+ ts: Date.now(),
286
+ };
287
+ // 先落本地 chain (open)
288
+ await writeChain({ ask, dissents: [], aligns: [], status: 'open', createdAt: Date.now() });
289
+ // 上链
290
+ const ok = await transport.sendMessage(peerDID, 'judgment_ask', encodeAskFrame(ask));
291
+ console.log(`[judgment-protocol] ask -> ${peerDID.slice(0, 12)}: "${decision.slice(0, 40)}..." (${ok ? 'SENT' : 'FAIL'})`);
292
+ judgmentEventBus.emit('judgment', { kind: 'ask_sent', askId: ask.askId, peerDID });
293
+ return ask;
294
+ }
295
+ async function sendDissentInternal(transport, peerDID, dissent) {
296
+ // 落本地 chain
297
+ const chain = await readChain(dissent.askId);
298
+ if (chain && !chain.dissents.some((d) => d.dissentId === dissent.dissentId)) {
299
+ chain.dissents.push(dissent);
300
+ await writeChain(chain);
301
+ }
302
+ // 上链
303
+ const ok = await transport.sendMessage(peerDID, 'judgment_dissent', encodeDissentFrame(dissent));
304
+ console.log(`[judgment-protocol] dissent -> ${peerDID.slice(0, 12)}: ${dissent.dissents.length} 反方 (${ok ? 'SENT' : 'FAIL'})`);
305
+ judgmentEventBus.emit('judgment', { kind: 'dissent_sent', dissentId: dissent.dissentId, askId: dissent.askId, peerDID });
306
+ }
307
+ /**
308
+ * 显式补发 dissent (例如收到 ask 后 LLM 还没好, 后续可重发更准的).
309
+ * 协议上 dissent 在 ask 收到时自动回发, 这个 API 是给"主动想换反方"用的.
310
+ */
311
+ export async function sendDissent(peerDID, askId, dissents, transport = defaultIrohTransport) {
312
+ ensureListeners(transport);
313
+ if (dissents.length < 3 || dissents.length > 5) {
314
+ throw new Error(`协议硬约束: dissent 必须 3-5 个, 实际 ${dissents.length}`);
315
+ }
316
+ const myNodeId = transport.getNodeId() || 'unknown';
317
+ const dissent = {
318
+ dissentId: crypto.randomUUID(),
319
+ askId,
320
+ fromNodeId: myNodeId,
321
+ dissents,
322
+ ts: Date.now(),
323
+ };
324
+ await sendDissentInternal(transport, peerDID, dissent);
325
+ return dissent;
326
+ }
327
+ /**
328
+ * align — 显式协议消息, 必须基于 askId + 双方 dissentIds.
329
+ * 协议校验: basedOn.dissentIds 必须非空 (否则视为"未基于互见").
330
+ */
331
+ export async function sendAlign(peerDID, askId, conclusion, dissentIds, transport = defaultIrohTransport) {
332
+ ensureListeners(transport);
333
+ if (dissentIds.length === 0) {
334
+ throw new Error('协议硬约束: align 必须引用至少 1 条 dissent (否则不是基于反方互见)');
335
+ }
336
+ const myNodeId = transport.getNodeId() || 'unknown';
337
+ const align = {
338
+ alignId: crypto.randomUUID(),
339
+ askId,
340
+ fromNodeId: myNodeId,
341
+ conclusion,
342
+ basedOn: { askId, dissentIds },
343
+ ts: Date.now(),
344
+ };
345
+ // 落本地 chain
346
+ const chain = await readChain(askId);
347
+ if (chain) {
348
+ chain.aligns.push(align);
349
+ chain.status = 'aligned';
350
+ chain.closedAt = Date.now();
351
+ await writeChain(chain);
352
+ }
353
+ // 上链
354
+ const ok = await transport.sendMessage(peerDID, 'judgment_align', encodeAlignFrame(align));
355
+ console.log(`[judgment-protocol] align -> ${peerDID.slice(0, 12)}: "${conclusion.slice(0, 40)}..." (${ok ? 'SENT' : 'FAIL'})`);
356
+ judgmentEventBus.emit('judgment', { kind: 'align_sent', alignId: align.alignId, askId, peerDID });
357
+ return align;
358
+ }
359
+ /**
360
+ * reflect — 把整链 (ask + 所有 dissent + 所有 align) 沉淀成 1 个 HumanJudgment,
361
+ * 写进 ~/.bolloon/judgments/yaml/{askId}.yaml, 并调 storeHumanJudgment 注入判断力库.
362
+ * 这是协议的"复利起点": 下次 distillDissent 会看到这条.
363
+ */
364
+ export async function reflect(askId, transport = defaultIrohTransport) {
365
+ const chain = await readChain(askId);
366
+ if (!chain)
367
+ return null;
368
+ if (chain.status === 'reflected') {
369
+ console.warn(`[judgment-protocol] ask ${askId} 已 reflect 过, 跳过`);
370
+ return null;
371
+ }
372
+ // 1) 序列化整链成 yaml (手搓, 避免引 js-yaml 依赖)
373
+ const yamlLines = [];
374
+ yamlLines.push(`# bolloon judgment reflection`);
375
+ yamlLines.push(`# generated: ${new Date().toISOString()}`);
376
+ yamlLines.push(`askId: ${chain.ask.askId}`);
377
+ yamlLines.push(`decision: ${yamlEscape(chain.ask.decision)}`);
378
+ yamlLines.push(`proposer: ${chain.ask.proposerNodeId}`);
379
+ yamlLines.push(`createdAt: "${new Date(chain.createdAt).toISOString()}"`);
380
+ yamlLines.push(`status: reflected`);
381
+ yamlLines.push('');
382
+ yamlLines.push(`dissents:`);
383
+ for (const d of chain.dissents) {
384
+ yamlLines.push(` - from: ${d.fromNodeId}`);
385
+ yamlLines.push(` ts: "${new Date(d.ts).toISOString()}"`);
386
+ yamlLines.push(` points:`);
387
+ for (const p of d.dissents) {
388
+ yamlLines.push(` - ${yamlEscape(p)}`);
389
+ }
390
+ }
391
+ yamlLines.push('');
392
+ yamlLines.push(`aligns:`);
393
+ for (const a of chain.aligns) {
394
+ yamlLines.push(` - from: ${a.fromNodeId}`);
395
+ yamlLines.push(` ts: "${new Date(a.ts).toISOString()}"`);
396
+ yamlLines.push(` conclusion: ${yamlEscape(a.conclusion)}`);
397
+ yamlLines.push(` basedOnDissentIds: [${a.basedOn.dissentIds.join(', ')}]`);
398
+ }
399
+ yamlLines.push('');
400
+ await ensureDirs();
401
+ const yPath = path.join(yamlDir(), `${askId}.yaml`);
402
+ await fs.writeFile(yPath, yamlLines.join('\n'), 'utf-8');
403
+ // 2) 注入判断力库 (供下次 distillDissent 用)
404
+ let judgmentId = '';
405
+ try {
406
+ // 找出我方的 dissent (如果有) 作为本节点判断
407
+ const myNodeId = transport.getNodeId() || '';
408
+ const myDissent = chain.dissents.find((d) => d.fromNodeId === myNodeId);
409
+ const lastAlign = chain.aligns[chain.aligns.length - 1];
410
+ const stored = await storeHumanJudgment({
411
+ decision: `judgment-protocol: ${chain.ask.decision.slice(0, 80)}`,
412
+ reasoning: [
413
+ `ask: ${chain.ask.decision}`,
414
+ myDissent ? `my-dissent: ${myDissent.dissents.join(' / ')}` : '',
415
+ lastAlign ? `conclusion: ${lastAlign.conclusion}` : '',
416
+ ].filter(Boolean),
417
+ confidence: chain.aligns.length > 0 ? 0.8 : 0.5,
418
+ domain: 'judgment-protocol',
419
+ tags: ['judgment-protocol', `ask:${askId.slice(0, 8)}`],
420
+ source: 'judgment-protocol',
421
+ });
422
+ judgmentId = stored.id;
423
+ }
424
+ catch (e) {
425
+ console.warn('[judgment-protocol] storeHumanJudgment 失败 (yaml 已写):', e.message);
426
+ }
427
+ // 3) 更新 chain 状态
428
+ chain.status = 'reflected';
429
+ chain.closedAt = Date.now();
430
+ await writeChain(chain);
431
+ console.log(`[judgment-protocol] reflect 完成: askId=${askId.slice(0, 8)} yaml=${yPath}`);
432
+ judgmentEventBus.emit('judgment', { kind: 'reflected', askId, judgmentId });
433
+ return { judgmentId, yamlPath: yPath };
434
+ }
435
+ /** 读 ask 的整链 (UI / CLI 用) */
436
+ export async function getChain(askId) {
437
+ return readChain(askId);
438
+ }
439
+ /** 列所有 ask (按时间倒序) */
440
+ export async function listChains(limit = 50) {
441
+ await ensureDirs();
442
+ const files = (await fs.readdir(chainsDir()))
443
+ .filter((f) => f.endsWith('.json'))
444
+ .sort()
445
+ .reverse()
446
+ .slice(0, limit);
447
+ const out = [];
448
+ for (const f of files) {
449
+ try {
450
+ const buf = await fs.readFile(path.join(chainsDir(), f), 'utf-8');
451
+ out.push(JSON.parse(buf));
452
+ }
453
+ catch { }
454
+ }
455
+ return out;
456
+ }
457
+ // yaml 字符串转义: 包单引号, 单引号变两个
458
+ function yamlEscape(s) {
459
+ if (!s)
460
+ return "''";
461
+ // 单行无特殊字符 → 直接包单引号
462
+ if (!/['"\\:#\n]/.test(s) && s.length < 200)
463
+ return `'${s.replace(/'/g, "''")}'`;
464
+ // 多行 → block scalar
465
+ return `|\n ${s.replace(/\n/g, '\n ')}`;
466
+ }
467
+ // ============================================================================
468
+ // 顶层导出 (CLI / Web 调)
469
+ // ============================================================================
470
+ export const judgmentProtocol = {
471
+ init: initJudgmentProtocol,
472
+ sendAsk,
473
+ sendDissent,
474
+ sendAlign,
475
+ reflect,
476
+ distillDissent,
477
+ getChain,
478
+ listChains,
479
+ };