@bolloon/bolloon-agent 0.1.34 → 0.1.35

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 (60) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/Bolloon.md +103 -0
  4. package/dist/agents/pi-sdk.js +264 -12
  5. package/dist/bootstrap/bootstrap.js +114 -0
  6. package/dist/bootstrap/context-collector.js +296 -0
  7. package/dist/bootstrap/lifecycle-hooks.js +109 -0
  8. package/dist/bootstrap/project-context.js +151 -0
  9. package/dist/index.js +11 -0
  10. package/dist/llm/pi-ai.js +31 -21
  11. package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
  12. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  13. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  14. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  15. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  16. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  17. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  18. package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
  19. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  20. package/dist/security/builtin-guards.js +124 -0
  21. package/dist/security/context-router-tool.js +106 -0
  22. package/dist/security/react-harness.js +143 -0
  23. package/dist/security/tool-gate.js +235 -0
  24. package/dist/utils/auto-evolve-policy.js +117 -0
  25. package/dist/utils/clamp.js +7 -0
  26. package/dist/utils/double.js +6 -0
  27. package/dist/web/client.js +668 -204
  28. package/dist/web/index.html +24 -4
  29. package/dist/web/server.js +531 -10
  30. package/lefthook.yml +29 -0
  31. package/package.json +3 -2
  32. package/scripts/auto-evolve-loop.ts +376 -0
  33. package/scripts/auto-evolve-oneshot.sh +155 -0
  34. package/scripts/auto-evolve-snapshot.sh +136 -0
  35. package/scripts/detect-schema-changes.sh +48 -0
  36. package/scripts/diff-reviewer.ts +159 -0
  37. package/scripts/weekly-report.ts +364 -0
  38. package/src/agents/pi-sdk.ts +293 -15
  39. package/src/bootstrap/bootstrap.ts +132 -0
  40. package/src/bootstrap/context-collector.ts +342 -0
  41. package/src/bootstrap/lifecycle-hooks.ts +176 -0
  42. package/src/bootstrap/project-context.ts +163 -0
  43. package/src/index.ts +11 -0
  44. package/src/llm/pi-ai.ts +33 -22
  45. package/src/security/builtin-guards.ts +162 -0
  46. package/src/security/context-router-tool.ts +122 -0
  47. package/src/security/react-harness.ts +177 -0
  48. package/src/security/tool-gate.ts +294 -0
  49. package/src/utils/auto-evolve-policy.ts +138 -0
  50. package/src/utils/clamp.ts +5 -0
  51. package/src/web/client.js +668 -204
  52. package/src/web/index.html +24 -4
  53. package/src/web/server.ts +596 -10
  54. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  55. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  56. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  57. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  58. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  59. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  60. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
@@ -0,0 +1 @@
1
+ 2026-06-14:6
@@ -0,0 +1 @@
1
+ auto-evolve-baseline-20260614T110128Z
package/Bolloon.md ADDED
@@ -0,0 +1,103 @@
1
+ # Bolloon
2
+
3
+ > 一个本地优先的 P2P AI 智能体网络。每台机器运行一个 bolloon,自动积累人类判断力,跨机器互联互通。
4
+
5
+ ## 架构总览
6
+
7
+ ```
8
+ +------------------- 单台 bolloon 进程 -------------------+
9
+ | |
10
+ | Web Server (port 54188) ← 用户聊天 / 配置 / 调试 |
11
+ | ↓ |
12
+ | Pi Agent (LLM 推理, 注入门, 监控门) |
13
+ | ↓ |
14
+ | Judgment System (判断力库 + 类 B 自适应) |
15
+ | ↓ |
16
+ | P2P Network (跨机器互联, v3 + P2PDirect 双轨) |
17
+ | |
18
+ +--------------------------------------------------------+
19
+ ↕ P2P (跨机器, 端到端加密)
20
+ [其它 bolloon 节点]
21
+ ```
22
+
23
+ **核心循环**:人类聊天 → 注入门检索相关判断力 → LLM 推理 → AI 回复 → 监控门审计 → 行为记录到 usage.jsonl → 类 B 自适应扫描 → 演化日志。
24
+
25
+ ## 目录结构
26
+
27
+ | 路径 | 职责 |
28
+ |---|---|
29
+ | `src/agents/pi-sdk.ts` | PiAgent 主类 / ReAct 循环 / 工具注册 / SessionStart/Stop hook 接入点 |
30
+ | `src/pi-ecosystem-judgment/` | **判断力系统核心**:注入门 / 蒸馏 / 演化 / 自适应扫描 / 监控门 |
31
+ | `src/web/server.ts` | Web server 主入口 (port 54188), 所有 REST API |
32
+ | `src/web/client.js` | 前端 (timeline panel, 反向引用链接, 自适应 tab) |
33
+ | `src/network/` | P2P 网络层 (iroh-bootstrap, p2p-direct) |
34
+ | `src/constraint-runtime/` | Workflow / 技能注册 / 会话持久化 |
35
+ | `src/social/` | 全局共享上下文 / 智能体心跳 / 协作任务 |
36
+ | `src/bootstrap/` | **本轮新增** — 启动上下文收集 / SessionStart/Stop/cron 入口 |
37
+ | `~/.bolloon/` | 用户数据目录 (本机资产, 不进 git) |
38
+
39
+ ## 关键设计意图
40
+
41
+ ### 判断力库 = 本机资产 (跨机器不同步)
42
+
43
+ 每台 bolloon 维护自己的判断力库 `~/.bolloon/human-values/judgments.json`。
44
+ **不**通过 P2P 广播 / 同步——这是原则。判断力沉淀人类偏好,是私有的。
45
+ 跨机器共享只通过 P2P RPC 临时调用,**判断力本身不流出去**。
46
+
47
+ ### 注入门 vs D 触发 vs 监控门 (3 道独立门)
48
+
49
+ | 门 | 触发时机 | 行为 | 写什么 |
50
+ |---|---|---|---|
51
+ | 注入门 (P0) | 每次 LLM chat 之前 | 检索相关判断力拼到 system prompt | `usage.jsonl` |
52
+ | D 触发 (D 路径) | AI 回复后 5min 节流 + async | 自动捕获新判断力 (蒸馏对话) | `judgments.json` |
53
+ | 监控门 (P3) | AI 回复后 fire-and-forget | 审计 AI 是否违反已注入原则 | `violations.jsonl` |
54
+
55
+ 3 道门都**静默失败**——任何 1 道挂掉不影响主对话。
56
+
57
+ ### 类 B 自适应扫描"只读不写"
58
+
59
+ 每天 0:00 定时跑 `runAdaptiveScan()`,扫 `usage.jsonl` + `judgments.json`,
60
+ 输出建议 (rising / stale / unused),**不**自动改库。
61
+ 用户在 UI "📊 自适应" tab 接受/拒绝,所有动作写 `evolution.jsonl` 留痕。
62
+ **可逆**是核心设计——AI 不能"自己改自己"。
63
+
64
+ ### 反向引用链接 + timeline panel
65
+
66
+ AI 回复下方挂极简 `📎 参考 N 条原则` 链接(不展开内嵌)。
67
+ 整个运行过程显示在 `loop-timeline-panel`(Claude Code 风格),按时间追加 phase / token / tool 事件。
68
+
69
+ ## 运行约定
70
+
71
+ | 命令 | 用途 |
72
+ |---|---|
73
+ | `npm run dev` | 启动 web server (http://localhost:54188) |
74
+ | `npm run start` | 同上但生产模式 |
75
+ | `npm test` | 跑 vitest |
76
+ | `npm run typecheck` | tsc --noEmit |
77
+
78
+ 数据存 `~/.bolloon/`:
79
+ - `human-values/judgments.json` — 判断力库
80
+ - `human-values/usage.jsonl` — 注入门使用记录
81
+ - `human-values/violations.jsonl` — 监控门违规记录
82
+ - `human-values/evolution.jsonl` — 类 B 自适应演化日志
83
+ - `persona.json` — 主人身份
84
+ - `sessions/<channel>/` — 每个 channel 的会话持久化
85
+ - `skills/` — 用户级 skills (与 .bolloon/skills/ 项目级并存)
86
+
87
+ ## 开发约定 (不要随意改)
88
+
89
+ 1. **v3 P2P + P2PDirect 双轨**——v3 是新协议,P2PDirect 是 fallback。两套都活着
90
+ 2. **判断力库是本机资产**——绝对不同步、不广播
91
+ 3. **类 B 不自动改库**——所有 AI 自动调整走 UI 接受 + evolution.jsonl 留痕
92
+ 4. **任何 hook 静默失败**——主对话不能因为 hook 挂掉而卡住
93
+ 5. **新模块加到 `src/pi-ecosystem-judgment/`**——这是判断力系统的归宿,别散落
94
+ 6. **前端 DOM 改完要 dist/web 重新构建**(如果有 build step)
95
+
96
+ ## 已知边界
97
+
98
+ - **类 B 自适应**每天 0:00 跑 1 次,重启 bolloon 期间不补跑
99
+ - **Context 缓存 24h**——中途改 Bolloon.md / persona / git commit 后, 重启才生效
100
+ - **PreToolUse hook 还没接**——agent 仍能调 `rm -rf`(已有 shell-guard 基础, hook 形式未接)
101
+ - **Stop hook 写 session 摘要**——不重复写完整 session 持久化(已存在)
102
+ - **跨 channel Context**——目前不分 channel, 单 channel 维度
103
+ - **embedding 检索**——上轮做了软相似度 (bigram) 兜底, 真 embedding 等 5k+ 条库再做
@@ -20,6 +20,15 @@ import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartb
20
20
  import { getGlobalSharedContext } from '../social/global-shared-context.js';
21
21
  import { Session, SkillRegistry, saveSession, loadSession } from '@bolloon/constraint-runtime';
22
22
  import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
23
+ // Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
24
+ // 失败静默, 不阻塞主对话
25
+ import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
26
+ // 持续监控门 (P3): AI 回复后审计是否违反原则
27
+ import { monitorAfterReply } from '../pi-ecosystem-judgment/monitor-gate.js';
28
+ // Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
29
+ import { onSessionStart, onStop, onPreToolUse } from '../pi-ecosystem-judgment/human-value-pipeline.js';
30
+ // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
31
+ import { ReactHarness } from '../security/react-harness.js';
23
32
  const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
24
33
  const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
25
34
  export class PiSessionManager {
@@ -341,8 +350,61 @@ class PiAgentSession {
341
350
  coordinator = new AgentCoordinator(3);
342
351
  harness = null;
343
352
  harnessEnabled = false;
353
+ /** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
354
+ reactHarness = new ReactHarness();
344
355
  usePivotLoop = false;
345
356
  pivotLoopConfig;
357
+ /**
358
+ * Judgment 注入门临时结果: 在 prompt / promptStream / promptWithPivotLoop 入口算一次, 拼到本轮 systemPrompt 末尾
359
+ * 每次调用都会重置 (避免上一轮遗留)
360
+ */
361
+ judgmentGateAddition = '';
362
+ judgmentGateUsedIds = [];
363
+ /**
364
+ * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
365
+ * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
366
+ */
367
+ currentOnStream = null;
368
+ currentSignal = null;
369
+ /** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
370
+ bootstrapAddition = '';
371
+ /** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
372
+ promptStartTime = 0;
373
+ /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
374
+ currentChannelId = '';
375
+ /**
376
+ * 算 judgment 注入门: 失败静默, 不阻塞主对话
377
+ * 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
378
+ * 调用方负责用完即清 (judgmentGateAddition='')
379
+ */
380
+ async computeJudgmentGate(input) {
381
+ const safePhase = (phase, extra = {}) => {
382
+ try {
383
+ if (this.currentOnStream) {
384
+ this.currentOnStream({ type: 'phase', phase, ...extra, content: '' });
385
+ }
386
+ }
387
+ catch { /* 静默 */ }
388
+ };
389
+ safePhase('gate_compute', { detail: '正在检索相关判断力...' });
390
+ try {
391
+ const gate = await injectJudgmentGate(input);
392
+ this.judgmentGateAddition = gate.systemAddition;
393
+ this.judgmentGateUsedIds = gate.usedIds;
394
+ if (gate.usedIds.length > 0) {
395
+ safePhase('gate_done', { usedCount: gate.usedIds.length });
396
+ }
397
+ }
398
+ catch (err) {
399
+ console.warn('[PiAgent] judgment gate failed (non-fatal):', err);
400
+ this.judgmentGateAddition = '';
401
+ this.judgmentGateUsedIds = [];
402
+ }
403
+ }
404
+ clearJudgmentGate() {
405
+ this.judgmentGateAddition = '';
406
+ this.judgmentGateUsedIds = [];
407
+ }
346
408
  constructor(config) {
347
409
  this.cwd = config.cwd;
348
410
  this.peerId = config.peerId || 'local';
@@ -428,10 +490,14 @@ class PiAgentSession {
428
490
  const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
429
491
  this.harness = createBollharnessIntegration();
430
492
  this.harnessEnabled = true;
493
+ // ReactHarness 已用 bollharness, 这里也记一份以供 archive 调用
494
+ this.reactHarness = new ReactHarness({ harnessEnabled: true, gateEnabled: true });
431
495
  }
432
496
  catch (e) {
433
497
  console.warn('[PiAgentSession] Harness initialization failed:', e);
434
498
  this.harnessEnabled = false;
499
+ // 失败 fallback: 走纯 8-gate (不带 bollharness 的 8-gate 工作流)
500
+ this.reactHarness = new ReactHarness({ harnessEnabled: false, gateEnabled: true });
435
501
  }
436
502
  }
437
503
  registerTools() {
@@ -767,8 +833,9 @@ class PiAgentSession {
767
833
  return false;
768
834
  }
769
835
  }
770
- async prompt(input) {
836
+ async prompt(input, options) {
771
837
  this.minimaxAvailable = this.checkMinimax();
838
+ this.currentChannelId = options?.channelId ?? this.currentChannelId;
772
839
  this.messageHistory.push({
773
840
  role: 'user',
774
841
  content: input
@@ -778,10 +845,25 @@ class PiAgentSession {
778
845
  this.messageHistory.push({ role: 'assistant', content: response });
779
846
  return response;
780
847
  }
781
- return this.runReActLoop();
848
+ // P0 注入门
849
+ this.currentSignal = options?.signal ?? null;
850
+ this.currentOnStream = options?.onStream ?? null;
851
+ await this.computeJudgmentGate(input);
852
+ try {
853
+ return await this.runReActLoop(undefined, options?.signal);
854
+ }
855
+ finally {
856
+ if (this.judgmentGateUsedIds.length > 0) {
857
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
858
+ }
859
+ this.clearJudgmentGate();
860
+ this.currentSignal = null;
861
+ this.currentOnStream = null;
862
+ }
782
863
  }
783
- async promptStream(input, onStream) {
864
+ async promptStream(input, onStream, signal, channelId) {
784
865
  this.minimaxAvailable = this.checkMinimax();
866
+ this.currentChannelId = channelId ?? this.currentChannelId;
785
867
  this.messageHistory.push({
786
868
  role: 'user',
787
869
  content: input
@@ -793,11 +875,61 @@ class PiAgentSession {
793
875
  onStream({ type: 'done', content: '' });
794
876
  return response;
795
877
  }
796
- const result = await this.runReActLoop(onStream);
878
+ // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
879
+ this.currentOnStream = onStream;
880
+ this.currentSignal = signal ?? null;
881
+ await this.computeJudgmentGate(input);
882
+ // Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
883
+ // (失败静默, 5s 限流防止循环)
884
+ let bootstrapAddition = '';
885
+ try {
886
+ const ss = await onSessionStart({ channelId: this.currentChannelId || undefined });
887
+ bootstrapAddition = ss.systemAddition || '';
888
+ }
889
+ catch (err) {
890
+ console.warn('[PiAgent] onSessionStart failed (non-fatal):', err);
891
+ }
892
+ this.bootstrapAddition = bootstrapAddition;
893
+ this.promptStartTime = Date.now();
894
+ let result;
895
+ try {
896
+ result = await this.runReActLoop(onStream, signal);
897
+ }
898
+ catch (err) {
899
+ // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
900
+ this.currentOnStream = null;
901
+ this.currentSignal = null;
902
+ throw err;
903
+ }
797
904
  onStream({ type: 'done', content: '' });
905
+ // 回溯: 异步记录 usage (不等)
906
+ if (this.judgmentGateUsedIds.length > 0) {
907
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
908
+ // P0.5: 把 usedIds 通过 stream 事件回传给调用方 (server.ts 写到 session message)
909
+ try {
910
+ onStream({ type: 'used_judgments', usedIds: this.judgmentGateUsedIds, content: '' });
911
+ }
912
+ catch { }
913
+ }
914
+ // P3 监控门: fire-and-forget 审计 AI 回复是否违反原则
915
+ monitorAfterReply(input, result);
916
+ // Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
917
+ const stopStartTime = this.promptStartTime || Date.now();
918
+ onStop({
919
+ channelId: this.currentChannelId || 'unknown',
920
+ durationMs: Date.now() - stopStartTime,
921
+ usedJudgmentIds: [...this.judgmentGateUsedIds],
922
+ }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
923
+ // 用完即清, 避免污染下一轮
924
+ this.clearJudgmentGate();
925
+ this.currentOnStream = null;
926
+ this.currentSignal = null;
927
+ this.bootstrapAddition = '';
928
+ this.promptStartTime = 0;
798
929
  return result;
799
930
  }
800
- async promptWithPivotLoop(input, config) {
931
+ async promptWithPivotLoop(input, config, channelId) {
932
+ this.currentChannelId = channelId ?? this.currentChannelId;
801
933
  if (!this.minimaxAvailable) {
802
934
  const response = await this.handleFallback(input);
803
935
  return {
@@ -824,12 +956,14 @@ class PiAgentSession {
824
956
  for (const tool of this.tools.values()) {
825
957
  loop.registerTool(tool);
826
958
  }
959
+ // P0 注入门: 在构造 systemPrompt 之前算一次, 拼到末尾
960
+ await this.computeJudgmentGate(input);
827
961
  const personaSection = this.persona ? `
828
962
  角色描述: ${this.persona.description || '无'}
829
963
  性格特点: ${this.persona.personality || '无'}
830
964
  问候语: ${this.persona.greeting || '无'}
831
965
  ` : '';
832
- const systemPrompt = `你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
966
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
833
967
  当前工作目录: ${this.cwd}
834
968
  当前身份: ${this.identity.name} (${this.identity.did})
835
969
 
@@ -846,15 +980,20 @@ ${this.getToolDefinitions()}
846
980
  - 每次只调用一个工具
847
981
  - 仔细分析工具返回结果
848
982
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
849
- - 如果需要更多信息,继续调用工具`;
983
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
850
984
  const result = await loop.execute(input, llm, systemPrompt);
851
985
  this.messageHistory.push({ role: 'user', content: input });
852
986
  if (result.response) {
853
987
  this.messageHistory.push({ role: 'assistant', content: result.response });
854
988
  }
989
+ // 回溯 + 清场
990
+ if (this.judgmentGateUsedIds.length > 0) {
991
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
992
+ }
993
+ this.clearJudgmentGate();
855
994
  return result;
856
995
  }
857
- async runReActLoop(onStream) {
996
+ async runReActLoop(onStream, signal) {
858
997
  const llm = getMinimax();
859
998
  let iteration = 0;
860
999
  let finalResponse = '';
@@ -869,6 +1008,14 @@ ${this.getToolDefinitions()}
869
1008
  if (onStream) {
870
1009
  onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
871
1010
  }
1011
+ // React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
1012
+ // 失败静默 (fail-open), 不阻塞主循环
1013
+ try {
1014
+ await this.reactHarness.onSessionStart(this.currentChannelId || undefined);
1015
+ }
1016
+ catch (err) {
1017
+ console.warn('[PiAgent] reactHarness.onSessionStart failed (non-fatal):', err);
1018
+ }
872
1019
  while (iteration < this.MAX_REACT_ITERATIONS) {
873
1020
  iteration++;
874
1021
  // 调试日志:显示每次循环开始
@@ -892,7 +1039,7 @@ ${this.getToolDefinitions()}
892
1039
  性格特点: ${this.persona.personality || '无'}
893
1040
  问候语: ${this.persona.greeting || '无'}
894
1041
  ` : '';
895
- const systemPrompt = `你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1042
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
896
1043
  当前工作目录: ${this.cwd}
897
1044
  当前身份: ${this.identity.name} (${this.identity.did})
898
1045
  ${refineContext}
@@ -910,8 +1057,8 @@ ${toolDefs}
910
1057
  - 每次只调用一个工具
911
1058
  - 仔细分析工具返回结果
912
1059
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
913
- - 如果需要更多信息,继续调用工具`;
914
- const response = await llm.chat(context, systemPrompt);
1060
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
1061
+ const response = await llm.chat(context, systemPrompt, signal);
915
1062
  const reply = response.reply.trim();
916
1063
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
917
1064
  console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
@@ -954,9 +1101,104 @@ ${toolDefs}
954
1101
  console.warn(`[PiAgent] 未知工具: ${toolCall.name},跳过并继续`);
955
1102
  continue;
956
1103
  }
1104
+ // Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
1105
+ // 失败静默 — hook 自身挂掉 = 放行
1106
+ let toolToExecute = tool;
1107
+ try {
1108
+ const pre = await onPreToolUse({ tool: toolCall.name, args: toolCall.args || {} });
1109
+ if (!pre.allowed) {
1110
+ const deniedResult = {
1111
+ success: false,
1112
+ error: `PreToolUse 拒绝: ${pre.reason || '未通过安全校验'}`,
1113
+ };
1114
+ this.messageHistory.push({
1115
+ role: 'tool',
1116
+ content: JSON.stringify(deniedResult),
1117
+ toolResult: deniedResult,
1118
+ });
1119
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1120
+ if (onStream) {
1121
+ onStream({
1122
+ type: 'error',
1123
+ content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1124
+ tool: toolCall.name,
1125
+ });
1126
+ }
1127
+ console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
1128
+ // 不调 tool.execute, 也不计 consecutiveErrors (这是用户级拒绝, 不是工具错)
1129
+ continue;
1130
+ }
1131
+ }
1132
+ catch (err) {
1133
+ console.warn('[PiAgent] onPreToolUse failed (non-fatal, allowing):', err);
1134
+ }
1135
+ // React Harness: 8-gate + builtin-guards 校验 (在 PreToolUse 之后, 串接双层)
1136
+ // 失败静默, 拒绝时不调 tool.execute
1137
+ try {
1138
+ const pre = await this.reactHarness.preToolCall(toolCall.name, toolCall.args || {}, this.currentChannelId || undefined);
1139
+ if (!pre.allowed) {
1140
+ const deniedResult = {
1141
+ success: false,
1142
+ error: `Harness gate 拒绝 (${pre.details.rejectedBy}): ${pre.reason || '未通过安全校验'}`,
1143
+ };
1144
+ this.messageHistory.push({
1145
+ role: 'tool',
1146
+ content: JSON.stringify(deniedResult),
1147
+ toolResult: deniedResult,
1148
+ });
1149
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1150
+ if (onStream) {
1151
+ onStream({
1152
+ type: 'error',
1153
+ content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1154
+ tool: toolCall.name,
1155
+ });
1156
+ }
1157
+ console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
1158
+ continue;
1159
+ }
1160
+ }
1161
+ catch (err) {
1162
+ console.warn('[PiAgent] reactHarness.preToolCall failed (non-fatal, allowing):', err);
1163
+ }
957
1164
  try {
958
- const result = await tool.execute(toolCall.args);
1165
+ let result = await tool.execute(toolCall.args);
959
1166
  console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success}`);
1167
+ // Context router: 拿最近一次 preToolCall 算的 hint, 拼到 tool result messageHistory
1168
+ // (LLM 下次看到 tool result 时, 能"记得"这次调用的安全约束)
1169
+ const routeHint = this.reactHarness.getLastRouteHint();
1170
+ if (routeHint && routeHint.systemAddition) {
1171
+ this.messageHistory.push({
1172
+ role: 'system',
1173
+ content: `[Harness Router Hint: ${routeHint.reason}]\n${routeHint.systemAddition}`,
1174
+ });
1175
+ this.reactHarness.clearRouteHint();
1176
+ }
1177
+ // React Harness: post-tool call (output 审计: secret leak 等)
1178
+ // 拒绝时 result.output 含敏感 → 替换为 generic message, 不污染 messageHistory
1179
+ try {
1180
+ const post = await this.reactHarness.postToolCall(toolCall.name, String(result.output || ''), this.currentChannelId || undefined);
1181
+ if (!post.allowed) {
1182
+ if (onStream) {
1183
+ onStream({
1184
+ type: 'error',
1185
+ content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${post.reason || '输出含敏感信息'}`,
1186
+ tool: toolCall.name,
1187
+ });
1188
+ }
1189
+ console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${post.reason}`);
1190
+ // 替换 result: success 仍保留 (tool 本身没错), 但 output 改成 generic
1191
+ // 这样 LLM 下轮看 output 不会拿到秘密, 但 success 标志让它知道 "工具执行了"
1192
+ result = {
1193
+ ...result,
1194
+ output: `[harness output gate: output 含敏感内容, 已屏蔽. 原因: ${post.reason || 'unknown'}]`,
1195
+ _harnessDenied: true,
1196
+ };
1197
+ }
1198
+ }
1199
+ catch (err) {
1200
+ console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
1201
+ }
960
1202
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
961
1203
  this.logToHarness(toolCall.name, toolCall.args, result);
962
1204
  // 通知前端工具执行结果
@@ -1092,6 +1334,13 @@ Workspace root folder: ${this.cwd}
1092
1334
  `;
1093
1335
  finalResponse = identityPrefix + finalResponse;
1094
1336
  this.messageHistory.push({ role: 'assistant', content: finalResponse });
1337
+ // React Harness: 循环结束
1338
+ try {
1339
+ await this.reactHarness.onSessionEnd();
1340
+ }
1341
+ catch (err) {
1342
+ console.warn('[PiAgent] reactHarness.onSessionEnd failed (non-fatal):', err);
1343
+ }
1095
1344
  return finalResponse;
1096
1345
  }
1097
1346
  async deepThink(prompt) {
@@ -1561,6 +1810,9 @@ ${this.extractOperationsFromRef(operationsRef)}
1561
1810
  updateIdentity(updates) {
1562
1811
  this.identity = { ...this.identity, ...updates };
1563
1812
  }
1813
+ setCurrentChannelId(channelId) {
1814
+ this.currentChannelId = channelId;
1815
+ }
1564
1816
  getSessionState() {
1565
1817
  return this.sessionManager.getState();
1566
1818
  }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Bolloon Bootstrap — 启动入口
3
+ *
4
+ * web server 启动时 (或 CLI 模式) 调一次, 完成 3 件事:
5
+ * 1. 跑类 B 自适应扫描 (暖缓存 + 写 evolution.jsonl 启动事件)
6
+ * 2. 收集项目 Context (Bolloon.md / git / persona / judgments / skills)
7
+ * 3. 挂每天 0:00 定时任务
8
+ *
9
+ * 失败静默: 任意步骤失败 console.warn, 不抛错 (主流程不被阻塞)
10
+ */
11
+ import { runAdaptiveScan, logEvolution } from '../pi-ecosystem-judgment/adaptive-scan.js';
12
+ import { collectBolloonContext } from './context-collector.js';
13
+ /**
14
+ * 入口: web server / CLI 启动时调一次
15
+ */
16
+ export async function bootstrapBolloon(opts = {}) {
17
+ const start = Date.now();
18
+ const errors = [];
19
+ // 1. 类 B 启动扫描
20
+ let scanResult = {
21
+ scannedAt: new Date().toISOString(),
22
+ judgmentsTotal: 0,
23
+ usageEntriesScanned: 0,
24
+ suggestions: [],
25
+ };
26
+ try {
27
+ scanResult = await runAdaptiveScan();
28
+ await logEvolution({
29
+ ts: new Date().toISOString(),
30
+ action: 'accept', // 用 accept 表示"系统记录" (跟 reject 区分)
31
+ suggestion: {
32
+ key: 'bootstrap-startup',
33
+ kind: 'unused', // 占位
34
+ judgmentId: '__bootstrap__',
35
+ decision: 'Bolloon 启动扫描',
36
+ reason: `本次启动扫描了 ${scanResult.judgmentsTotal} 条原则, ${scanResult.usageEntriesScanned} 条使用记录, 生成 ${scanResult.suggestions.length} 条建议`,
37
+ action: 'review',
38
+ metrics: { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 },
39
+ scannedAt: scanResult.scannedAt,
40
+ },
41
+ });
42
+ console.log(`[bootstrap] 类 B 启动扫描完成: ${scanResult.suggestions.length} 条建议`);
43
+ }
44
+ catch (err) {
45
+ errors.push(`scan: ${err.message}`);
46
+ console.warn('[bootstrap] 启动扫描失败 (非致命):', err);
47
+ }
48
+ // 2. 收集项目 Context
49
+ let context = {
50
+ projectRoot: opts.cwd ?? process.cwd(),
51
+ projectName: 'unknown',
52
+ bolloonMd: null,
53
+ git: null,
54
+ persona: null,
55
+ judgmentsSummary: { total: 0, active: 0, superseded: 0, rejected: 0, topValues: [] },
56
+ skills: [],
57
+ env: { os: 'unknown', nodeVersion: 'unknown', llmProvider: 'unknown' },
58
+ pending: { goals: [], todos: [] },
59
+ collectedAt: new Date().toISOString(),
60
+ };
61
+ try {
62
+ context = await collectBolloonContext({ cwd: opts.cwd ?? process.cwd() });
63
+ console.log(`[bootstrap] context 收集完成: ${context.judgmentsSummary.total} judgments, ${context.skills.length} skills`);
64
+ }
65
+ catch (err) {
66
+ errors.push(`context: ${err.message}`);
67
+ console.warn('[bootstrap] context 收集失败 (非致命):', err);
68
+ }
69
+ // 3. 挂定时任务 (每天 0:00 跑扫描, server 重启时丢失可接受)
70
+ try {
71
+ scheduleAdaptiveScanDaily();
72
+ console.log('[bootstrap] 定时任务已挂: 每天 0:00 跑类 B 扫描');
73
+ }
74
+ catch (err) {
75
+ errors.push(`schedule: ${err.message}`);
76
+ console.warn('[bootstrap] 定时任务挂载失败 (非致命):', err);
77
+ }
78
+ const durationMs = Date.now() - start;
79
+ console.log(`[bootstrap] 完成 (${durationMs}ms, ${errors.length} 个错误)`);
80
+ return { context, scanResult, durationMs, errors };
81
+ }
82
+ // ============================================================
83
+ // 定时任务: 每天 0:00 跑类 B 自适应扫描
84
+ // ============================================================
85
+ let scheduled = false;
86
+ function scheduleAdaptiveScanDaily() {
87
+ if (scheduled)
88
+ return;
89
+ scheduled = true;
90
+ const now = new Date();
91
+ const next = new Date(now);
92
+ next.setHours(24, 0, 0, 0);
93
+ const msUntilMidnight = next.getTime() - now.getTime();
94
+ // 第一次: 等到明天 0:00
95
+ setTimeout(() => {
96
+ runAdaptiveScan().then((result) => {
97
+ console.log(`[bootstrap] 定时扫描完成: ${result.suggestions.length} 条建议`);
98
+ }).catch((err) => {
99
+ console.warn('[bootstrap] 定时扫描失败:', err);
100
+ });
101
+ // 之后: 每 24h
102
+ setInterval(() => {
103
+ runAdaptiveScan().then((result) => {
104
+ console.log(`[bootstrap] 定时扫描完成: ${result.suggestions.length} 条建议`);
105
+ }).catch((err) => {
106
+ console.warn('[bootstrap] 定时扫描失败:', err);
107
+ });
108
+ }, 24 * 60 * 60 * 1000);
109
+ }, msUntilMidnight);
110
+ }
111
+ /** 测试辅助: 重置 scheduled 标志 */
112
+ export function _resetScheduleForTest() {
113
+ scheduled = false;
114
+ }