@bolloon/bolloon-agent 0.1.33 → 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.
- package/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/README.md +7 -2
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bollharness-integration/index.js +8 -1
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/heartbeat/Watchdog.js +9 -1
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/network/p2p-direct.js +59 -2
- package/dist/pi-ecosystem/index.js +9 -6
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/decision.js +5 -2
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/social/heartbeat.js +19 -2
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/api-config.html +3 -3
- package/dist/web/client.js +1328 -351
- package/dist/web/index.html +34 -31
- package/dist/web/server.js +1128 -58
- package/dist/web/style.css +370 -0
- package/lefthook.yml +29 -0
- package/package.json +4 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bollharness-integration/index.ts +8 -32
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/heartbeat/Watchdog.ts +9 -1
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/network/p2p-direct.ts +59 -3
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/social/ant-colony/index.js +19 -0
- package/src/social/heartbeat.ts +18 -2
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/api-config.html +3 -3
- package/src/web/client.js +1328 -351
- package/src/web/index.html +34 -31
- package/src/web/server.ts +1179 -53
- package/src/web/style.css +370 -0
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
- package/src/social/ant-colony/AdaptiveHeartbeat.ts +0 -131
- package/src/social/ant-colony/PheromoneEngine.ts +0 -302
- package/src/social/ant-colony/index.ts +0 -18
- package/src/social/ant-colony/types.ts +0 -94
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bolloon Context Collector — 启动时一次扫描, 收集 5 类项目状态
|
|
3
|
+
*
|
|
4
|
+
* 设计原则:
|
|
5
|
+
* - 每个收集器独立, 失败静默 (返回 null/[] 而非抛错)
|
|
6
|
+
* - 整体 < 200ms (git log + 文件扫描)
|
|
7
|
+
* - 结果可缓存 24h (跟类 B 自适应一致)
|
|
8
|
+
*
|
|
9
|
+
* 收集维度:
|
|
10
|
+
* 1. 项目层: projectRoot, projectName, Bolloon.md 全文
|
|
11
|
+
* 2. git 层: branch, last 5 commits, uncommitted changes count
|
|
12
|
+
* 3. persona 层: ~/.bolloon/persona.json
|
|
13
|
+
* 4. judgments 层: 摘要 (total / active / superseded / top values)
|
|
14
|
+
* 5. skills 层: ~/.bolloon/skills/ + .bolloon/skills/
|
|
15
|
+
* 6. 环境层: OS / node / llm provider
|
|
16
|
+
* 7. pending 层: ~/.bolloon/goals/* + src/ 下的 TODO/FIXME
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { execFile } from 'child_process';
|
|
22
|
+
import { promisify } from 'util';
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
// ============================================================
|
|
25
|
+
// 单个收集器: 全部独立, 失败静默
|
|
26
|
+
// ============================================================
|
|
27
|
+
async function safeReadFile(filePath, maxBytes) {
|
|
28
|
+
try {
|
|
29
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
30
|
+
if (content.length > maxBytes) {
|
|
31
|
+
return content.substring(0, maxBytes) + '\n... (truncated)';
|
|
32
|
+
}
|
|
33
|
+
return content;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function safeExecFile(cmd, args, cwd) {
|
|
40
|
+
try {
|
|
41
|
+
const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 5000 });
|
|
42
|
+
return stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function collectProject(cwd, bolloonMdMaxBytes) {
|
|
49
|
+
let projectName = path.basename(cwd);
|
|
50
|
+
try {
|
|
51
|
+
const pkgRaw = await fs.readFile(path.join(cwd, 'package.json'), 'utf-8');
|
|
52
|
+
const pkg = JSON.parse(pkgRaw);
|
|
53
|
+
if (typeof pkg.name === 'string')
|
|
54
|
+
projectName = pkg.name;
|
|
55
|
+
}
|
|
56
|
+
catch { /* ignore */ }
|
|
57
|
+
const bolloonMd = await safeReadFile(path.join(cwd, 'Bolloon.md'), bolloonMdMaxBytes);
|
|
58
|
+
return { projectName, bolloonMd };
|
|
59
|
+
}
|
|
60
|
+
async function collectGit(cwd, limit) {
|
|
61
|
+
const branchOut = await safeExecFile('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
62
|
+
if (!branchOut)
|
|
63
|
+
return null;
|
|
64
|
+
const logOut = await safeExecFile('git', ['log', '--oneline', `-n`, String(limit)], cwd);
|
|
65
|
+
const statusOut = await safeExecFile('git', ['status', '--porcelain'], cwd);
|
|
66
|
+
return {
|
|
67
|
+
branch: branchOut,
|
|
68
|
+
lastCommits: logOut ? logOut.split('\n').filter(Boolean) : [],
|
|
69
|
+
uncommittedChanges: statusOut ? statusOut.split('\n').filter(Boolean).length : 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function collectPersona() {
|
|
73
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
74
|
+
const raw = await safeReadFile(path.join(home, '.bolloon', 'persona.json'), 5000);
|
|
75
|
+
if (!raw)
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
const p = JSON.parse(raw);
|
|
79
|
+
return {
|
|
80
|
+
name: String(p.name || 'unknown'),
|
|
81
|
+
description: String(p.description || ''),
|
|
82
|
+
personality: String(p.personality || ''),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function collectJudgmentsSummary(topN) {
|
|
90
|
+
// 用动态 import 避免循环依赖
|
|
91
|
+
const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
92
|
+
let all = [];
|
|
93
|
+
try {
|
|
94
|
+
all = await loadAllJudgments();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { total: 0, active: 0, superseded: 0, rejected: 0, topValues: [] };
|
|
98
|
+
}
|
|
99
|
+
const byStatus = { active: 0, superseded: 0, rejected: 0 };
|
|
100
|
+
for (const j of all) {
|
|
101
|
+
const s = (j.status ?? 'active');
|
|
102
|
+
if (s in byStatus)
|
|
103
|
+
byStatus[s]++;
|
|
104
|
+
}
|
|
105
|
+
// 借用 getRelevantValues 算 top values (已经在 store 里)
|
|
106
|
+
let topValues = [];
|
|
107
|
+
try {
|
|
108
|
+
const { getRelevantValues } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
109
|
+
const values = await getRelevantValues('安全 代码 质量 测试 文档 用户'); // 通用 query
|
|
110
|
+
topValues = values.slice(0, topN).map((v) => ({
|
|
111
|
+
category: v.category,
|
|
112
|
+
value: v.value,
|
|
113
|
+
weight: v.weight,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
return {
|
|
118
|
+
total: all.length,
|
|
119
|
+
active: byStatus.active,
|
|
120
|
+
superseded: byStatus.superseded,
|
|
121
|
+
rejected: byStatus.rejected,
|
|
122
|
+
topValues,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function collectSkills() {
|
|
126
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
127
|
+
const userSkillsDir = path.join(home, '.bolloon', 'skills');
|
|
128
|
+
const projectSkillsDir = path.join(process.cwd(), '.bolloon', 'skills');
|
|
129
|
+
const out = [];
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
for (const dir of [userSkillsDir, projectSkillsDir]) {
|
|
132
|
+
try {
|
|
133
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
134
|
+
for (const e of entries) {
|
|
135
|
+
if (!e.isDirectory() || seen.has(e.name))
|
|
136
|
+
continue;
|
|
137
|
+
seen.add(e.name);
|
|
138
|
+
// 尝试读 SKILL.md 第一行作为描述
|
|
139
|
+
let description = '';
|
|
140
|
+
try {
|
|
141
|
+
const skillMd = await fs.readFile(path.join(dir, e.name, 'SKILL.md'), 'utf-8');
|
|
142
|
+
// 抓第一段非空非 frontmatter
|
|
143
|
+
const lines = skillMd.split('\n');
|
|
144
|
+
let inFrontmatter = false;
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
if (line.trim() === '---') {
|
|
147
|
+
inFrontmatter = !inFrontmatter;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (inFrontmatter)
|
|
151
|
+
continue;
|
|
152
|
+
if (line.trim() && !line.startsWith('#')) {
|
|
153
|
+
description = line.trim().substring(0, 120);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch { /* ignore */ }
|
|
159
|
+
out.push({ name: e.name, description });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { /* dir 不存在正常 */ }
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function collectEnv() {
|
|
167
|
+
let llmProvider = 'unknown';
|
|
168
|
+
try {
|
|
169
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
170
|
+
const cfg = require(path.join(home, '.bolloon', 'llm-config.json'));
|
|
171
|
+
if (cfg && typeof cfg === 'object' && 'provider' in cfg) {
|
|
172
|
+
llmProvider = String(cfg.provider);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch { /* ignore */ }
|
|
176
|
+
return {
|
|
177
|
+
os: `${os.platform()} ${os.release()}`,
|
|
178
|
+
nodeVersion: process.version,
|
|
179
|
+
llmProvider,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function collectPending(opts) {
|
|
183
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
184
|
+
const goalsDir = path.join(home, '.bolloon', 'goals');
|
|
185
|
+
const goals = [];
|
|
186
|
+
try {
|
|
187
|
+
const entries = await fs.readdir(goalsDir, { withFileTypes: true });
|
|
188
|
+
for (const e of entries) {
|
|
189
|
+
if (e.isFile())
|
|
190
|
+
goals.push(e.name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch { /* dir 不存在 */ }
|
|
194
|
+
// 扫 TODO/FIXME (限制 20 条避免太长)
|
|
195
|
+
const todoScanDir = opts.todoScanDir ?? path.join(opts.cwd, 'src');
|
|
196
|
+
const todos = await scanTodos(todoScanDir, 20);
|
|
197
|
+
return { goals, todos };
|
|
198
|
+
}
|
|
199
|
+
async function scanTodos(dir, limit) {
|
|
200
|
+
const out = [];
|
|
201
|
+
try {
|
|
202
|
+
await walkDir(dir, async (filePath) => {
|
|
203
|
+
if (!/\.(ts|js|tsx|jsx)$/.test(filePath))
|
|
204
|
+
return;
|
|
205
|
+
// 跳过 dist / node_modules
|
|
206
|
+
if (filePath.includes('/node_modules/') || filePath.includes('/dist/'))
|
|
207
|
+
return;
|
|
208
|
+
try {
|
|
209
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
210
|
+
const lines = content.split('\n');
|
|
211
|
+
for (let i = 0; i < lines.length; i++) {
|
|
212
|
+
const line = lines[i];
|
|
213
|
+
// 匹配 TODO / FIXME / XXX 注释
|
|
214
|
+
const m = line.match(/(?:\/\/|\/\*|\*|<!--)\s*(TODO|FIXME|XXX|HACK)[::]?\s*(.+)/);
|
|
215
|
+
if (m) {
|
|
216
|
+
out.push({
|
|
217
|
+
file: path.relative(dir, filePath),
|
|
218
|
+
line: i + 1,
|
|
219
|
+
text: m[2].trim().substring(0, 80),
|
|
220
|
+
});
|
|
221
|
+
if (out.length >= limit)
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch { /* ignore */ }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch { /* dir 不存在 */ }
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
async function walkDir(dir, cb) {
|
|
233
|
+
let entries;
|
|
234
|
+
try {
|
|
235
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
for (const e of entries) {
|
|
241
|
+
const full = path.join(dir, e.name);
|
|
242
|
+
if (e.isDirectory()) {
|
|
243
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.'))
|
|
244
|
+
continue;
|
|
245
|
+
await walkDir(full, cb);
|
|
246
|
+
}
|
|
247
|
+
else if (e.isFile()) {
|
|
248
|
+
await cb(full);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ============================================================
|
|
253
|
+
// 主入口
|
|
254
|
+
// ============================================================
|
|
255
|
+
export async function collectBolloonContext(opts) {
|
|
256
|
+
const { cwd, bolloonMdMaxBytes = 2000, gitCommitLimit = 5, topValuesLimit = 10, } = opts;
|
|
257
|
+
// 并行收集 (除 judgments 因为要动态 import + 依赖其它)
|
|
258
|
+
const [project, git, persona, skills, env, pending] = await Promise.all([
|
|
259
|
+
collectProject(cwd, bolloonMdMaxBytes),
|
|
260
|
+
collectGit(cwd, gitCommitLimit),
|
|
261
|
+
collectPersona(),
|
|
262
|
+
collectSkills(),
|
|
263
|
+
Promise.resolve(collectEnv()),
|
|
264
|
+
collectPending(opts),
|
|
265
|
+
]);
|
|
266
|
+
// judgments 单独调 (内部 import)
|
|
267
|
+
const judgmentsSummary = await collectJudgmentsSummary(topValuesLimit);
|
|
268
|
+
return {
|
|
269
|
+
projectRoot: cwd,
|
|
270
|
+
projectName: project.projectName,
|
|
271
|
+
bolloonMd: project.bolloonMd,
|
|
272
|
+
git,
|
|
273
|
+
persona,
|
|
274
|
+
judgmentsSummary,
|
|
275
|
+
skills,
|
|
276
|
+
env,
|
|
277
|
+
pending,
|
|
278
|
+
collectedAt: new Date().toISOString(),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// ============================================================
|
|
282
|
+
// 24h 缓存 (跟类 B 一致)
|
|
283
|
+
// ============================================================
|
|
284
|
+
let cached = null;
|
|
285
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
286
|
+
export async function getCachedBolloonContext(opts, force = false) {
|
|
287
|
+
if (!force && cached && cached.cwd === opts.cwd && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
288
|
+
return cached.ctx;
|
|
289
|
+
}
|
|
290
|
+
const ctx = await collectBolloonContext(opts);
|
|
291
|
+
cached = { at: Date.now(), ctx, cwd: opts.cwd };
|
|
292
|
+
return ctx;
|
|
293
|
+
}
|
|
294
|
+
export function clearBolloonContextCache() {
|
|
295
|
+
cached = null;
|
|
296
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle Hooks — SessionStart / Stop / PreToolUse
|
|
3
|
+
*
|
|
4
|
+
* 失败静默: 任意 hook 挂掉不抛错, 仅 console.warn.
|
|
5
|
+
* 任何调用方都可以放心 await, 不会阻塞主对话.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { getCachedBolloonContext, clearBolloonContextCache } from './context-collector.js';
|
|
11
|
+
import { formatContextForSystemPrompt } from './project-context.js';
|
|
12
|
+
let lastSessionStartAt = 0;
|
|
13
|
+
const MIN_INTERVAL_MS = 5000; // 同一进程 5s 内最多触发一次, 防止循环
|
|
14
|
+
export async function onSessionStart(opts = {}) {
|
|
15
|
+
const start = Date.now();
|
|
16
|
+
if (start - lastSessionStartAt < MIN_INTERVAL_MS) {
|
|
17
|
+
// 限流: 返回空 (调用方已经有缓存, 不需要重算)
|
|
18
|
+
return { systemAddition: '', collectMs: 0, truncated: false };
|
|
19
|
+
}
|
|
20
|
+
lastSessionStartAt = start;
|
|
21
|
+
try {
|
|
22
|
+
const ctx = await getCachedBolloonContext({ cwd: opts.cwd ?? process.cwd() }, opts.force ?? false);
|
|
23
|
+
let systemAddition = formatContextForSystemPrompt(ctx, { maxChars: opts.maxChars });
|
|
24
|
+
// 在头部加 channel 标识 (供 LLM 知道当前对话归属)
|
|
25
|
+
if (opts.channelId) {
|
|
26
|
+
systemAddition = `# 当前 channel: ${opts.channelId}\n\n` + systemAddition;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
systemAddition,
|
|
30
|
+
collectMs: Date.now() - start,
|
|
31
|
+
truncated: systemAddition.length > 0 && systemAddition.includes('截断模式'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.warn('[lifecycle-hooks] onSessionStart failed (silent):', err);
|
|
36
|
+
return { systemAddition: '', collectMs: Date.now() - start, truncated: false };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function clearSessionStartCache() {
|
|
40
|
+
lastSessionStartAt = 0;
|
|
41
|
+
clearBolloonContextCache();
|
|
42
|
+
}
|
|
43
|
+
const STOP_LOG = (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/sessions';
|
|
44
|
+
/**
|
|
45
|
+
* 把本次 session 摘要写 ~/.bolloon/sessions/<channelId>/last-stop.json
|
|
46
|
+
* 不重复写完整 session 持久化 (已由 createWebServer 内的 saveSession 处理)
|
|
47
|
+
*/
|
|
48
|
+
export async function onStop(opts) {
|
|
49
|
+
try {
|
|
50
|
+
const dir = path.join(STOP_LOG, sanitizeChannelId(opts.channelId));
|
|
51
|
+
await fs.mkdir(dir, { recursive: true });
|
|
52
|
+
const file = path.join(dir, 'last-stop.json');
|
|
53
|
+
const entry = {
|
|
54
|
+
ts: new Date().toISOString(),
|
|
55
|
+
channelId: opts.channelId,
|
|
56
|
+
messages: opts.messages ?? 0,
|
|
57
|
+
durationMs: opts.durationMs,
|
|
58
|
+
usedJudgmentIds: opts.usedJudgmentIds ?? [],
|
|
59
|
+
};
|
|
60
|
+
await fs.writeFile(file, JSON.stringify(entry, null, 2), 'utf-8');
|
|
61
|
+
return { persisted: true, path: file };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.warn('[lifecycle-hooks] onStop failed (silent):', err);
|
|
65
|
+
return { persisted: false, path: null };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function sanitizeChannelId(id) {
|
|
69
|
+
// 防止路径穿越
|
|
70
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
71
|
+
}
|
|
72
|
+
// 黑名单: 高危 shell 命令模式
|
|
73
|
+
const DANGEROUS_PATTERNS = [
|
|
74
|
+
{ re: /\brm\s+(-[a-z]*f[a-z]*\s+)?-[a-z]*r[a-z]*\s+\//, reason: '禁止递归删除根目录' },
|
|
75
|
+
{ re: /\bgit\s+push\s+.*--force\b/, reason: '禁止 force push' },
|
|
76
|
+
{ re: /\brm\s+-rf\s+~\//, reason: '禁止递归删除 home' },
|
|
77
|
+
{ re: /\bdd\s+if=.*\s+of=\/dev\//, reason: '禁止 dd 覆盖块设备' },
|
|
78
|
+
{ re: /\bcurl\s+.*\|\s*(ba)?sh\b/, reason: '禁止 curl|sh 直执行' },
|
|
79
|
+
{ re: />\s*\/dev\/sd[a-z]/, reason: '禁止写裸设备' },
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* PreToolUse: 当前实现"危险命令拦截" (黑名单)
|
|
83
|
+
* 白名单机制未启用 (默认放行所有非黑名单)
|
|
84
|
+
* PreToolUse hook 接入 pi-sdk.ts 的 ReAct 循环是下个迭代
|
|
85
|
+
*/
|
|
86
|
+
export async function onPreToolUse(opts) {
|
|
87
|
+
try {
|
|
88
|
+
// 仅检查 shell 类工具的命令字符串
|
|
89
|
+
if (opts.tool === 'shell' || opts.tool === 'shell_exec' || opts.tool === 'bash') {
|
|
90
|
+
const cmd = String(opts.args.command || opts.args.cmd || '');
|
|
91
|
+
for (const { re, reason } of DANGEROUS_PATTERNS) {
|
|
92
|
+
if (re.test(cmd)) {
|
|
93
|
+
return { allowed: false, reason };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { allowed: true };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.warn('[lifecycle-hooks] onPreToolUse failed (silent):', err);
|
|
101
|
+
return { allowed: true }; // 失败放行 (不阻塞)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ============================================================
|
|
105
|
+
// 测试辅助: 获取 STOP_LOG 路径 (供测试覆盖路径)
|
|
106
|
+
// ============================================================
|
|
107
|
+
export function getStopLogPathForTest() {
|
|
108
|
+
return STOP_LOG;
|
|
109
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Context Formatter — BolloonContext → system prompt 片段
|
|
3
|
+
*
|
|
4
|
+
* 输出 markdown 文本, 直接拼到 LLM system prompt 头部
|
|
5
|
+
*
|
|
6
|
+
* 字符上限默认 4000 (system prompt 不能太长).
|
|
7
|
+
* 超限时按优先级砍: TODO 列表 → 压缩 Bolloon.md → 砍 judgments top values
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_MAX_CHARS = 4000;
|
|
10
|
+
const BOLLOON_MD_KEEP = 500; // 砍到这么长
|
|
11
|
+
export function formatContextForSystemPrompt(ctx, opts = {}) {
|
|
12
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push(`# 你的项目上下文 (自动 bootstrap, 时间: ${ctx.collectedAt})`);
|
|
15
|
+
lines.push('');
|
|
16
|
+
// 1. 项目名 + Bolloon.md 摘要
|
|
17
|
+
lines.push(`## 项目: ${ctx.projectName}`);
|
|
18
|
+
lines.push(`- 路径: ${ctx.projectRoot}`);
|
|
19
|
+
if (ctx.bolloonMd) {
|
|
20
|
+
const mdSummary = firstParagraphs(ctx.bolloonMd, 3);
|
|
21
|
+
lines.push(`- Bolloon.md 摘要:`);
|
|
22
|
+
lines.push(mdSummary);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
lines.push(`- Bolloon.md: (缺失, 建议创建)`);
|
|
26
|
+
}
|
|
27
|
+
lines.push('');
|
|
28
|
+
// 2. Git
|
|
29
|
+
if (ctx.git) {
|
|
30
|
+
lines.push(`## Git 状态`);
|
|
31
|
+
lines.push(`- branch: ${ctx.git.branch}`);
|
|
32
|
+
lines.push(`- 未提交变更: ${ctx.git.uncommittedChanges}`);
|
|
33
|
+
if (ctx.git.lastCommits.length > 0) {
|
|
34
|
+
lines.push(`- 最近提交:`);
|
|
35
|
+
for (const c of ctx.git.lastCommits) {
|
|
36
|
+
lines.push(` - ${c}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
lines.push('');
|
|
40
|
+
}
|
|
41
|
+
// 3. Persona
|
|
42
|
+
if (ctx.persona) {
|
|
43
|
+
lines.push(`## 主人身份 (persona)`);
|
|
44
|
+
lines.push(`- 名字: ${ctx.persona.name}`);
|
|
45
|
+
if (ctx.persona.description)
|
|
46
|
+
lines.push(`- 描述: ${ctx.persona.description}`);
|
|
47
|
+
if (ctx.persona.personality)
|
|
48
|
+
lines.push(`- 性格: ${ctx.persona.personality}`);
|
|
49
|
+
lines.push('');
|
|
50
|
+
}
|
|
51
|
+
// 4. Judgments
|
|
52
|
+
if (ctx.judgmentsSummary.total > 0) {
|
|
53
|
+
const j = ctx.judgmentsSummary;
|
|
54
|
+
lines.push(`## 已沉淀的判断力 (${j.total} 条)`);
|
|
55
|
+
lines.push(`- 活跃: ${j.active}, 已过时: ${j.superseded}, 已拒绝: ${j.rejected}`);
|
|
56
|
+
if (j.topValues.length > 0) {
|
|
57
|
+
const topStr = j.topValues.map((v) => `${v.category}/${v.value}`).join(', ');
|
|
58
|
+
lines.push(`- Top 价值偏好: [${topStr}]`);
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
}
|
|
62
|
+
// 5. Skills
|
|
63
|
+
if (ctx.skills.length > 0) {
|
|
64
|
+
lines.push(`## 工具 + Skills`);
|
|
65
|
+
lines.push(`- 已注册 skills (${ctx.skills.length}):`);
|
|
66
|
+
for (const s of ctx.skills) {
|
|
67
|
+
const desc = s.description ? ` — ${s.description}` : '';
|
|
68
|
+
lines.push(` - ${s.name}${desc}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
// 6. Pending
|
|
73
|
+
if (ctx.pending.goals.length > 0 || ctx.pending.todos.length > 0) {
|
|
74
|
+
lines.push(`## 未完成任务`);
|
|
75
|
+
if (ctx.pending.goals.length > 0) {
|
|
76
|
+
lines.push(`- ~/.bolloon/goals/: ${ctx.pending.goals.length} 条 (${ctx.pending.goals.slice(0, 3).join(', ')}${ctx.pending.goals.length > 3 ? '...' : ''})`);
|
|
77
|
+
}
|
|
78
|
+
if (ctx.pending.todos.length > 0) {
|
|
79
|
+
lines.push(`- 代码 TODO/FIXME: ${ctx.pending.todos.length} 处`);
|
|
80
|
+
for (const t of ctx.pending.todos.slice(0, 5)) {
|
|
81
|
+
lines.push(` - ${t.file}:${t.line} ${t.text}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
lines.push('');
|
|
85
|
+
}
|
|
86
|
+
// 7. 环境 (放最后, 信息密度低)
|
|
87
|
+
lines.push(`## 环境`);
|
|
88
|
+
lines.push(`- ${ctx.env.os}, Node ${ctx.env.nodeVersion}, LLM: ${ctx.env.llmProvider}`);
|
|
89
|
+
// 字符截断
|
|
90
|
+
let result = lines.join('\n');
|
|
91
|
+
if (result.length > maxChars) {
|
|
92
|
+
result = truncateContext(ctx, maxChars);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 超限时按优先级砍: TODO 列表 → 压缩 Bolloon.md → 砍 top values
|
|
98
|
+
*/
|
|
99
|
+
function truncateContext(ctx, maxChars) {
|
|
100
|
+
// 先砍 TODO 列表 (low value)
|
|
101
|
+
const todoCapped = ctx.pending.todos.slice(0, 3);
|
|
102
|
+
const goalsCapped = ctx.pending.goals.slice(0, 2);
|
|
103
|
+
// 再砍 Bolloon.md
|
|
104
|
+
const mdCapped = ctx.bolloonMd
|
|
105
|
+
? firstParagraphs(ctx.bolloonMd, 1, BOLLOON_MD_KEEP)
|
|
106
|
+
: null;
|
|
107
|
+
// 砍 top values
|
|
108
|
+
const topValuesCapped = ctx.judgmentsSummary.topValues.slice(0, 5);
|
|
109
|
+
const lines = [];
|
|
110
|
+
lines.push(`# 你的项目上下文 (自动 bootstrap, 时间: ${ctx.collectedAt}, 截断模式)`);
|
|
111
|
+
lines.push(`## 项目: ${ctx.projectName}`);
|
|
112
|
+
if (mdCapped) {
|
|
113
|
+
lines.push(`- Bolloon.md (压缩): ${mdCapped.split('\n')[0]?.substring(0, 100)}...`);
|
|
114
|
+
}
|
|
115
|
+
if (ctx.git) {
|
|
116
|
+
lines.push(`## Git: ${ctx.git.branch} (未提交: ${ctx.git.uncommittedChanges})`);
|
|
117
|
+
for (const c of ctx.git.lastCommits.slice(0, 3))
|
|
118
|
+
lines.push(` - ${c}`);
|
|
119
|
+
}
|
|
120
|
+
if (ctx.persona)
|
|
121
|
+
lines.push(`## Persona: ${ctx.persona.name}`);
|
|
122
|
+
if (ctx.judgmentsSummary.total > 0) {
|
|
123
|
+
lines.push(`## 判断力: ${ctx.judgmentsSummary.total} 条 (活跃 ${ctx.judgmentsSummary.active})`);
|
|
124
|
+
if (topValuesCapped.length > 0) {
|
|
125
|
+
lines.push(`- Top: ${topValuesCapped.map((v) => `${v.category}/${v.value}`).join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (ctx.skills.length > 0) {
|
|
129
|
+
lines.push(`## Skills (${ctx.skills.length}): ${ctx.skills.map((s) => s.name).join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
if (todoCapped.length > 0) {
|
|
132
|
+
lines.push(`## TODO (${todoCapped.length}/${ctx.pending.todos.length})`);
|
|
133
|
+
for (const t of todoCapped)
|
|
134
|
+
lines.push(` - ${t.file}:${t.line} ${t.text}`);
|
|
135
|
+
}
|
|
136
|
+
if (goalsCapped.length > 0) {
|
|
137
|
+
lines.push(`## Goals: ${goalsCapped.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 取前 N 段非空段落
|
|
143
|
+
*/
|
|
144
|
+
function firstParagraphs(text, count, maxLen) {
|
|
145
|
+
const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
|
|
146
|
+
let result = paragraphs.slice(0, count).join('\n\n');
|
|
147
|
+
if (maxLen && result.length > maxLen) {
|
|
148
|
+
result = result.substring(0, maxLen) + '...';
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
@@ -30,11 +30,19 @@ export class Watchdog {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* 记录活动(调用后更新 lastActivityTime)
|
|
33
|
+
* 2026-06-10: 加 5s 去抖, 防 broadcast / SSE 高频路径炸 log + CPU
|
|
34
|
+
* - lastActivityTime 始终更新 (cheap)
|
|
35
|
+
* - 但 onLog 回调只在距上次 ≥5s 时触发, 避免每秒几十次 console.log
|
|
33
36
|
*/
|
|
37
|
+
_lastLogAt = 0;
|
|
34
38
|
recordActivity(component) {
|
|
35
39
|
this.state.lastActivityTime = Date.now();
|
|
36
40
|
if (component) {
|
|
37
|
-
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
if (now - this._lastLogAt >= 5000) {
|
|
43
|
+
this._lastLogAt = now;
|
|
44
|
+
this.callbacks.onLog?.(`[Watchdog] Activity from ${component}`);
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1433,6 +1433,17 @@ async function main() {
|
|
|
1433
1433
|
s.warn('将使用无 P2P 模式运行');
|
|
1434
1434
|
}
|
|
1435
1435
|
await bootstrapIroh(keypair, name);
|
|
1436
|
+
// Bolloon Bootstrap: 启动扫描 + Context 收集 + 挂定时任务
|
|
1437
|
+
// 失败静默 (主流程不被阻塞)
|
|
1438
|
+
try {
|
|
1439
|
+
const { bootstrapBolloon } = await import('./pi-ecosystem-judgment/human-value-pipeline.js');
|
|
1440
|
+
s.info('正在 bootstrap bolloon 上下文...');
|
|
1441
|
+
const bs = await bootstrapBolloon({ cwd: process.cwd() });
|
|
1442
|
+
s.info(`Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
|
|
1443
|
+
}
|
|
1444
|
+
catch (err) {
|
|
1445
|
+
s.warn(`Bootstrap 失败 (非致命, 主流程继续): ${err.message}`);
|
|
1446
|
+
}
|
|
1436
1447
|
s.divider();
|
|
1437
1448
|
if (mode === 'web') {
|
|
1438
1449
|
const port = parseInt(process.env.PORT || '54188');
|