@bolloon/bolloon-agent 0.1.40 → 0.1.42

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 (38) hide show
  1. package/.comm/README.md +21 -0
  2. package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +7 -0
  3. package/CLAUDE.md +3 -0
  4. package/dist/agents/intent-classifier.js +99 -0
  5. package/dist/agents/pi-sdk.js +1364 -58
  6. package/dist/agents/pre-tool-validator.js +2 -1
  7. package/dist/agents/shell-guard.js +9 -3
  8. package/dist/agents/shell-tool.js +28 -13
  9. package/dist/agents/task-state.js +224 -0
  10. package/dist/agents/workflow-pivot-loop.js +30 -0
  11. package/dist/bollharness-integration/gate-state-machine.js +13 -0
  12. package/dist/bollharness-integration/integration.js +71 -0
  13. package/dist/bollharness-integration/skill-adapter.js +52 -127
  14. package/dist/bootstrap/context-hierarchy.js +6 -4
  15. package/dist/cli/interface.js +28 -0
  16. package/dist/documents/reader.js +14 -0
  17. package/dist/git-transport/chat-render.js +155 -0
  18. package/dist/git-transport/chat-repo.js +370 -0
  19. package/dist/git-transport/chat-types.js +23 -0
  20. package/dist/git-transport/chat-watch.js +102 -0
  21. package/dist/index.js +471 -25
  22. package/dist/llm/pi-ai.js +103 -27
  23. package/dist/network/local-inbox-bus.js +73 -0
  24. package/dist/network/p2p-direct.js +3 -4
  25. package/dist/pi-ecosystem-judgment/adaptive-scan.js +6 -2
  26. package/dist/pi-ecosystem-judgment/causal-judge.js +3 -3
  27. package/dist/pi-ecosystem-judgment/index.js +1 -1
  28. package/dist/security/tool-gate.js +11 -0
  29. package/dist/web/client.js +2876 -3770
  30. package/dist/web/components/p2p/index.js +226 -264
  31. package/dist/web/server.js +83 -15
  32. package/dist/web/ui/message-renderer.js +326 -442
  33. package/dist/web/ui/step-timeline.js +255 -351
  34. package/package.json +2 -2
  35. package/scripts/lefthook-helper.sh +69 -0
  36. package/dist/web/components/p2p/P2PModal.js +0 -188
  37. package/dist/web/components/p2p/p2p-modal.js +0 -657
  38. package/dist/web/components/p2p/p2p-tools.js +0 -248
@@ -88,6 +88,10 @@ function loadAllHarnessSkills() {
88
88
  * Base skill class for bollharness-compatible skills
89
89
  */
90
90
  export class BaseSkill {
91
+ static llm = null;
92
+ static setLlm(instance) {
93
+ BaseSkill.llm = instance;
94
+ }
91
95
  log(message, level = 'info') {
92
96
  const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : 'ℹ️';
93
97
  console.log(`${prefix} [${this.name}] ${message}`);
@@ -95,6 +99,33 @@ export class BaseSkill {
95
99
  formatOutput(output) {
96
100
  return JSON.stringify(output, null, 2);
97
101
  }
102
+ async callLm(system, user) {
103
+ let llm = BaseSkill.llm;
104
+ if (!llm) {
105
+ try {
106
+ const { getMinimax } = await import('../llm/pi-ai.js');
107
+ const m = getMinimax();
108
+ if (m && typeof m.chat === 'function') {
109
+ BaseSkill.setLlm(m);
110
+ llm = m;
111
+ }
112
+ }
113
+ catch {
114
+ return this.formatOutput({ warning: 'LLM 未注入', name: this.name });
115
+ }
116
+ }
117
+ if (!llm) {
118
+ return this.formatOutput({ warning: 'LLM 未注入', name: this.name });
119
+ }
120
+ try {
121
+ const res = await llm.chat([{ role: 'user', content: user }], system);
122
+ return res.reply || '(空回复)';
123
+ }
124
+ catch (err) {
125
+ this.log(`LLM 调用失败: ${err.message}`, 'error');
126
+ return this.formatOutput({ error: `LLM 调用失败: ${err.message}` });
127
+ }
128
+ }
98
129
  }
99
130
  /**
100
131
  * Architecture Skill - Project architect for architecture decisions
@@ -103,58 +134,12 @@ export class ArchSkill extends BaseSkill {
103
134
  name = 'arch';
104
135
  description = 'Project architect. Responsible for architecture decisions, scheme comparison, and boundary freezing.';
105
136
  async execute(params) {
106
- const task = params.task || params.description;
137
+ const task = (params.task || params.description || params.action || '');
107
138
  this.log('Analyzing architecture task');
108
- // Extract essence
109
- const essence = this.extractEssence(task);
110
- // Find tensions
111
- const tensions = this.identifyTensions(task);
112
- // Compare alternatives
113
- const alternatives = this.compareAlternatives(task);
114
- // Identify boundaries to freeze
115
- const boundaries = this.identifyBoundaries(task);
116
- return this.formatOutput({
117
- essence,
118
- tensions,
119
- alternatives,
120
- boundaries,
121
- recommendation: this.makeRecommendation(task, alternatives),
122
- });
123
- }
124
- extractEssence(task) {
125
- // Simplified essence extraction
126
- return `The core challenge is: ${task}`;
127
- }
128
- identifyTensions(task) {
129
- return [
130
- 'Simplicity vs Flexibility',
131
- 'Performance vs Maintainability',
132
- 'Coupling vs Cohesion',
133
- ];
134
- }
135
- compareAlternatives(task) {
136
- return [
137
- {
138
- name: 'Option A: Direct Implementation',
139
- tradeoffs: ['Fast to implement', 'May not scale'],
140
- recommendation: 'Suitable for MVP',
141
- },
142
- {
143
- name: 'Option B: Abstraction Layer',
144
- tradeoffs: ['More upfront work', 'Better for extension'],
145
- recommendation: 'Suitable for long-term',
146
- },
147
- ];
148
- }
149
- identifyBoundaries(task) {
150
- return [
151
- 'API contract boundaries',
152
- 'Data format boundaries',
153
- 'Capability boundaries',
154
- ];
155
- }
156
- makeRecommendation(task, alternatives) {
157
- return alternatives[1]?.recommendation || 'Consider abstraction layer for long-term maintainability';
139
+ if (!task) {
140
+ return this.formatOutput({ warning: '未提供任务', usage: '--harness-skill arch "你的架构任务"' });
141
+ }
142
+ return this.callLm('You are a senior software architect. Analyze the task and output structured JSON with fields: essence, tensions, alternatives, boundaries, recommendation.', `Architecture task: ${task}`);
158
143
  }
159
144
  }
160
145
  /**
@@ -221,18 +206,9 @@ export class LeadSkill extends BaseSkill {
221
206
  gate_pack: this.getGatePack(),
222
207
  });
223
208
  }
224
- classifyChange(params) {
209
+ async classifyChange(params) {
225
210
  const description = params.description || '';
226
- // Simplified classification
227
- const isPolicy = description.includes('policy') || description.includes('boundary');
228
- const isContract = description.includes('API') || description.includes('contract') || description.includes('schema');
229
- const isImplementation = !isPolicy && !isContract;
230
- return this.formatOutput({
231
- classification: isPolicy ? 'policy' : isContract ? 'contract' : 'implementation',
232
- description,
233
- minimum_gate_path: isPolicy ? '0→8 (full)' : isContract ? '0→8 (full + consumers)' : '0→7 (fast track eligible)',
234
- fast_track_eligible: isImplementation,
235
- });
211
+ return this.callLm('You are a change classifier. Given a description, classify the change as policy, contract, or implementation. Output JSON: { classification, description, minimum_gate_path, fast_track_eligible }.', `Classify this change: ${description}`);
236
212
  }
237
213
  }
238
214
  /**
@@ -242,44 +218,12 @@ export class TaskArchSkill extends BaseSkill {
242
218
  name = 'task-arch';
243
219
  description = 'Task decomposition. Breaks down PLAN into parallelizable work packages (WP).';
244
220
  async execute(params) {
245
- const plan = params.plan;
221
+ const plan = (params.plan || params.task || params.description || params.action || '');
246
222
  this.log('Decomposing plan into work packages');
247
- // Extract work packages
248
- const workPackages = this.extractWorkPackages(plan);
249
- // Identify seams
250
- const seams = this.identifySeams(workPackages);
251
- // Identify integration points
252
- const integration = this.identifyIntegration(workPackages);
253
- return this.formatOutput({
254
- work_packages: workPackages,
255
- seams,
256
- integration,
257
- seam_owners: this.assignSeamOwners(seams),
258
- });
259
- }
260
- extractWorkPackages(plan) {
261
- // Simplified WP extraction
262
- return [
263
- { id: 'WP-1', description: 'Core implementation', files: ['src/agents/*.ts'] },
264
- { id: 'WP-2', description: 'Network layer', files: ['src/network/*.ts'] },
265
- { id: 'WP-3', description: 'Testing', files: ['src/test/*.ts'] },
266
- ];
267
- }
268
- identifySeams(packages) {
269
- return [
270
- { from: 'WP-1', to: 'WP-2', interface: 'P2PNetwork interface' },
271
- { from: 'WP-1', to: 'WP-3', interface: 'Test fixtures' },
272
- ];
273
- }
274
- identifyIntegration(packages) {
275
- return ['Integration test at WP-1/WP-2 boundary'];
276
- }
277
- assignSeamOwners(seams) {
278
- const owners = {};
279
- for (const seam of seams) {
280
- owners[`${seam.from}/${seam.to}`] = 'integration-owner';
223
+ if (!plan) {
224
+ return this.formatOutput({ warning: '未提供 plan', usage: '--harness-skill task-arch "你的开发计划"' });
281
225
  }
282
- return owners;
226
+ return this.callLm('You are a task decomposition specialist. Given a plan, break it down into work packages (WP) with seams and integration points. Output JSON: { work_packages, seams, integration }.', `Plan: ${plan}`);
283
227
  }
284
228
  }
285
229
  /**
@@ -398,35 +342,12 @@ export class CrystalLearnSkill extends BaseSkill {
398
342
  name = 'crystal-learn';
399
343
  description = 'Extracts failure patterns and maintains invariants.';
400
344
  async execute(params) {
401
- const task = params.task;
402
- this.log('Extracting failure patterns');
403
- // Identify failure modes
404
- const failures = this.identifyFailures(task);
405
- // Extract patterns
406
- const patterns = this.extractPatterns(failures);
407
- // Generate invariants
408
- const invariants = this.generateInvariants(patterns);
409
- return this.formatOutput({
410
- failure_modes: failures,
411
- patterns,
412
- invariants,
413
- });
414
- }
415
- identifyFailures(task) {
416
- return [
417
- 'Truth source split',
418
- 'Verification decay',
419
- 'Orphaned seams',
420
- ];
421
- }
422
- extractPatterns(failures) {
423
- return failures.map(f => ({
424
- pattern: f,
425
- prevention: `Implement guard to prevent ${f}`,
426
- }));
427
- }
428
- generateInvariants(patterns) {
429
- return patterns.map(p => `INV: ${p.pattern} must not occur`);
345
+ const task = (params.task || params.description || params.action || '');
346
+ this.log('Extracting failure patterns from: ' + (task || '(空)'));
347
+ if (!task) {
348
+ return this.formatOutput({ warning: '未提供任务', usage: '--harness-skill crystal-learn "你的任务描述"' });
349
+ }
350
+ return this.callLm('You are a failure pattern analyst. Extract failure modes, patterns, and invariants from the task description. Output structured JSON with failure_modes, patterns, invariants.', `Task: ${task}`);
430
351
  }
431
352
  }
432
353
  /**
@@ -482,6 +403,10 @@ export class SkillAdapter {
482
403
  getSkill(name) {
483
404
  return this.registry.get(name);
484
405
  }
406
+ injectLlm(llm) {
407
+ BaseSkill.setLlm(llm);
408
+ this.log('LLM 已注入到所有 BaseSkill');
409
+ }
485
410
  listSkills() {
486
411
  return this.registry.list();
487
412
  }
@@ -43,13 +43,15 @@ export const DEFAULT_MAX_CHARS = {
43
43
  // ============================================================
44
44
  export function resolveUserPath(home) {
45
45
  const h = home ?? process.env.HOME ?? os.homedir() ?? '/tmp';
46
- return path.join(h, '.bolloon', 'Bolloon.md');
46
+ // path.posix.join 这些是配置/注入路径,跨平台语义稳定, 便于测试断言 + 注入 system prompt
47
+ // (Windows 文件系统也接受 '/' 分隔符, 所以 fs.readFile 不受影响)
48
+ return path.posix.join(h, '.bolloon', 'Bolloon.md');
47
49
  }
48
50
  export function resolveProjectPaths(cwd) {
49
51
  return {
50
- project: path.join(cwd, 'Bolloon.md'),
51
- projectRulesDir: path.join(cwd, '.claude', 'rules'),
52
- local: path.join(cwd, 'CLAUDE.local.md'),
52
+ project: path.posix.join(cwd, 'Bolloon.md'),
53
+ projectRulesDir: path.posix.join(cwd, '.claude', 'rules'),
54
+ local: path.posix.join(cwd, 'CLAUDE.local.md'),
53
55
  };
54
56
  }
55
57
  // ============================================================
@@ -1,4 +1,5 @@
1
1
  import * as readline from 'readline';
2
+ import { spawn } from 'child_process';
2
3
  import { documentReader } from '../documents/reader.js';
3
4
  import { getMinimax } from '../constraints/index.js';
4
5
  import { AgentProtocol } from '../agents/protocol.js';
@@ -63,6 +64,7 @@ export class CLIInterface {
63
64
  console.log(' send <peer> - 向指定节点发送消息');
64
65
  console.log(' broadcast - 广播任务到所有节点');
65
66
  console.log(' summary <text> - 总结文本');
67
+ console.log(' chat <消息正文> - 跨机聊天 (commits as messages, wrapper)');
66
68
  console.log(' help - 显示帮助');
67
69
  console.log(' exit - 退出\n');
68
70
  }
@@ -89,6 +91,9 @@ export class CLIInterface {
89
91
  case 'summary':
90
92
  await this.handleSummary(args.join(' '));
91
93
  break;
94
+ case 'chat':
95
+ await this.handleChat(args.join(' '));
96
+ break;
92
97
  case 'help':
93
98
  this.showHelp();
94
99
  break;
@@ -172,6 +177,29 @@ export class CLIInterface {
172
177
  console.log(result.summary);
173
178
  console.log(`\n质量评分: ${(result.qualityScore * 10).toFixed(1)}/10`);
174
179
  }
180
+ handleChat(text) {
181
+ return new Promise((resolve) => {
182
+ if (!text) {
183
+ console.log('请提供聊天内容: chat <消息正文>');
184
+ resolve();
185
+ return;
186
+ }
187
+ // 委派给 bolloon --chat-send, spawn 子进程, 同步输出
188
+ const child = spawn('npx', ['tsx', 'src/index.ts', '--chat-send', text], {
189
+ cwd: process.cwd(),
190
+ stdio: 'inherit',
191
+ });
192
+ child.on('close', (code) => {
193
+ if (code !== 0)
194
+ console.error(`chat 子进程退出码 ${code}`);
195
+ resolve();
196
+ });
197
+ child.on('error', (err) => {
198
+ console.error('chat 子进程启动失败:', err.message);
199
+ resolve();
200
+ });
201
+ });
202
+ }
175
203
  async shutdown() {
176
204
  console.log('\n正在关闭...');
177
205
  await p2pNetwork.shutdown();
@@ -16,6 +16,20 @@ export class DocumentReader {
16
16
  case '.yaml':
17
17
  case '.yml':
18
18
  case '.json':
19
+ // M3.5 (2026-06-17): agent 自读源码需要 — 之前 .ts 不支持, LLM 拿到空内容
20
+ // .ts/.tsx/.js/.jsx 都是纯文本, 直接 readFile
21
+ case '.ts':
22
+ case '.tsx':
23
+ case '.js':
24
+ case '.jsx':
25
+ case '.mjs':
26
+ case '.cjs':
27
+ case '.py':
28
+ case '.go':
29
+ case '.rs':
30
+ case '.java':
31
+ case '.sh':
32
+ case '.bash':
19
33
  text = await fs.readFile(filePath, 'utf-8');
20
34
  break;
21
35
  case '.pdf':
@@ -0,0 +1,155 @@
1
+ // src/git-transport/chat-render.ts
2
+ // 解析 markdown 消息文件 + 去重 key + 列表/过滤, 纯函数, 不碰 git.
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as crypto from 'crypto';
6
+ import { CHAT_PROTOCOL_VERSION } from './chat-types.js';
7
+ function tryRead(p) {
8
+ try {
9
+ return fs.readFileSync(p, 'utf8');
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ // 极简 frontmatter 解析: 不引 gray-matter 这种依赖
16
+ // 约定: 文件首行是 "---", 之后是 YAML-like 块 (key: value), 直到下一个 "---"
17
+ // 字段值只支持 string / number / 嵌套对象字面量; 对我们够用
18
+ function parseFrontmatter(raw) {
19
+ if (!raw.startsWith('---'))
20
+ return { fm: null, body: raw };
21
+ const lines = raw.split(/\r?\n/);
22
+ let end = -1;
23
+ for (let i = 1; i < lines.length; i++) {
24
+ if (lines[i].trim() === '---') {
25
+ end = i;
26
+ break;
27
+ }
28
+ }
29
+ if (end === -1)
30
+ return { fm: null, body: raw };
31
+ const headerLines = lines.slice(1, end);
32
+ const body = lines.slice(end + 1).join('\n').replace(/^\n+/, '');
33
+ const fm = { v: CHAT_PROTOCOL_VERSION };
34
+ let i = 0;
35
+ while (i < headerLines.length) {
36
+ const line = headerLines[i];
37
+ if (!line.trim() || line.trim().startsWith('#')) {
38
+ i++;
39
+ continue;
40
+ }
41
+ const m = /^([A-Za-z_][\w]*)\s*:\s*(.*)$/.exec(line);
42
+ if (!m) {
43
+ i++;
44
+ continue;
45
+ }
46
+ const key = m[1];
47
+ let val = m[2].trim();
48
+ if (val === '') {
49
+ // 嵌套对象 — 收集到下一个顶级 key 之前
50
+ const obj = {};
51
+ i++;
52
+ while (i < headerLines.length) {
53
+ const l2 = headerLines[i];
54
+ if (/^[A-Za-z_][\w]*\s*:/.test(l2))
55
+ break;
56
+ const m2 = /^(\s+)([A-Za-z_][\w]*)\s*:\s*(.*)$/.exec(l2);
57
+ if (m2) {
58
+ obj[m2[2]] = stripQuotes(m2[3].trim());
59
+ }
60
+ i++;
61
+ }
62
+ val = obj;
63
+ }
64
+ else {
65
+ val = stripQuotes(val);
66
+ // 数字字面量尽量 parse 成 number, 让 fm.v 严格比较能过
67
+ if (/^-?\d+(\.\d+)?$/.test(val)) {
68
+ const n = Number(val);
69
+ if (!Number.isNaN(n))
70
+ val = n;
71
+ }
72
+ i++;
73
+ }
74
+ fm[key] = val;
75
+ }
76
+ // 强校验
77
+ if (typeof fm.from !== 'string' || typeof fm.fromPk !== 'string' || typeof fm.ts !== 'string') {
78
+ return { fm: null, body: raw };
79
+ }
80
+ if (fm.v !== CHAT_PROTOCOL_VERSION) {
81
+ return { fm: null, body: raw };
82
+ }
83
+ return { fm: fm, body };
84
+ }
85
+ function stripQuotes(s) {
86
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
87
+ return s.slice(1, -1);
88
+ }
89
+ return s;
90
+ }
91
+ export function parseMessageFile(filePath, sha) {
92
+ const raw = tryRead(filePath);
93
+ if (raw === null)
94
+ return null;
95
+ const { fm, body } = parseFrontmatter(raw);
96
+ if (!fm)
97
+ return null;
98
+ return { filePath, frontmatter: fm, body, sha };
99
+ }
100
+ // 列出 .comm/<role>/*.md 下所有消息文件 (角色目录是按字母扫的, 不依赖 git)
101
+ export function listMessageFiles(repoDir) {
102
+ const comm = path.join(repoDir, '.comm');
103
+ let entries = [];
104
+ try {
105
+ entries = fs.readdirSync(comm, { withFileTypes: true });
106
+ }
107
+ catch {
108
+ return [];
109
+ }
110
+ const out = [];
111
+ for (const e of entries) {
112
+ if (!e.isDirectory())
113
+ continue;
114
+ if (e.name.startsWith('_'))
115
+ continue; // _state / _inbox 跳过
116
+ const roleDir = path.join(comm, e.name);
117
+ let files = [];
118
+ try {
119
+ files = fs.readdirSync(roleDir);
120
+ }
121
+ catch {
122
+ continue;
123
+ }
124
+ for (const f of files) {
125
+ if (!f.endsWith('.md'))
126
+ continue;
127
+ out.push(path.join(roleDir, f));
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+ export function listMessages(repoDir, withRole, limit) {
133
+ const files = listMessageFiles(repoDir);
134
+ const out = [];
135
+ for (const f of files) {
136
+ if (withRole && !f.includes(`${path.sep}.comm${path.sep}${withRole}${path.sep}`))
137
+ continue;
138
+ const msg = parseMessageFile(f, '');
139
+ if (msg)
140
+ out.push(msg);
141
+ }
142
+ out.sort((a, b) => a.frontmatter.ts.localeCompare(b.frontmatter.ts));
143
+ return typeof limit === 'number' ? out.slice(-limit) : out;
144
+ }
145
+ // 去重 key: (fromPk, ts, contentHash8) — 不论 P2P 还是 git 谁先到都收敛到同一键
146
+ export function dedupeKey(msg) {
147
+ const h = crypto.createHash('sha256').update(msg.body).digest('hex').slice(0, 8);
148
+ return `${msg.frontmatter.fromPk}:${msg.frontmatter.ts}:${h}`;
149
+ }
150
+ // 单行展示 — 给 chat-list / chat-watch 输出用
151
+ export function renderOneLine(msg) {
152
+ const time = msg.frontmatter.ts.replace('T', ' ').replace(/\.\d+Z$/, 'Z').replace('Z', '');
153
+ const preview = msg.body.split('\n')[0].slice(0, 80);
154
+ return `[${time} ${msg.frontmatter.from}] ${preview}`;
155
+ }