@bolloon/bolloon-agent 0.1.34 → 0.1.36
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/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
- package/Bolloon.md +144 -0
- package/Dive-into/CITATION.cff +17 -0
- package/Dive-into/LICENSE +25 -0
- package/Dive-into/README.md +598 -0
- package/Dive-into/README_zh.md +599 -0
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/README.md +1 -1
- package/dist/agents/p2p-chat-tools.js +6 -6
- package/dist/agents/permission-mode.js +115 -0
- package/dist/agents/pi-sdk.js +687 -25
- package/dist/agents/pre-tool-validator.js +194 -0
- package/dist/agents/workflow-pivot-loop.js +113 -12
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
- package/dist/bollharness/src/scripts/context_router.js +1 -1
- package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
- package/dist/bollharness/src/scripts/guard_router.js +1 -1
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
- package/dist/bollharness-integration/context-router.js +2 -2
- package/dist/bollharness-integration/guard-checker.js +1 -1
- package/dist/bootstrap/bootstrap.js +117 -0
- package/dist/bootstrap/context-collector.js +312 -0
- package/dist/bootstrap/context-hierarchy.js +218 -0
- package/dist/bootstrap/lifecycle-hooks.js +171 -0
- package/dist/bootstrap/project-context.js +158 -0
- package/dist/context-compaction/auto-compact.js +144 -0
- package/dist/context-compaction/budget-gate.js +28 -0
- package/dist/context-compaction/budget-reduce.js +35 -0
- package/dist/context-compaction/context-collapse.js +66 -0
- package/dist/context-compaction/index.js +21 -0
- package/dist/context-compaction/microcompact.js +51 -0
- package/dist/context-compaction/pipeline.js +123 -0
- package/dist/context-compaction/snip.js +45 -0
- package/dist/context-compaction/token-estimator.js +35 -0
- package/dist/context-compaction/types.js +19 -0
- package/dist/heartbeat/HealthMonitor.js +3 -2
- package/dist/index.js +49 -2
- package/dist/llm/llm-judgment-client.js +32 -30
- package/dist/llm/pi-ai.js +125 -28
- package/dist/llm/system-prompt/health.js +129 -0
- package/dist/llm/system-prompt/registry.js +246 -0
- package/dist/llm/system-prompt/strip-hibsml.js +51 -0
- package/dist/llm/tool-manifest/ask_user_input.js +35 -0
- package/dist/llm/tool-manifest/bash.js +23 -0
- package/dist/llm/tool-manifest/create_file.js +24 -0
- package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
- package/dist/llm/tool-manifest/image_search.js +24 -0
- package/dist/llm/tool-manifest/index.js +69 -0
- package/dist/llm/tool-manifest/mcp.js +43 -0
- package/dist/llm/tool-manifest/message_compose.js +31 -0
- package/dist/llm/tool-manifest/places.js +83 -0
- package/dist/llm/tool-manifest/present_files.js +21 -0
- package/dist/llm/tool-manifest/recipe.js +40 -0
- package/dist/llm/tool-manifest/recommend_apps.js +23 -0
- package/dist/llm/tool-manifest/str_replace.js +27 -0
- package/dist/llm/tool-manifest/types.js +7 -0
- package/dist/llm/tool-manifest/view.js +24 -0
- package/dist/llm/tool-manifest/weather.js +27 -0
- package/dist/llm/tool-manifest/web.js +51 -0
- package/dist/network/p2p-direct.js +23 -0
- package/dist/network/source-intent-broadcaster.js +203 -0
- package/dist/network/source-intent.js +100 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +279 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- 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 +208 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/input-scanner.js +223 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- 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/client.js +3810 -3830
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +60 -49
- package/dist/web/server.js +827 -124
- package/dist/web/style.css +531 -249
- package/dist/web/ui/message-renderer.js +463 -0
- package/dist/web/ui/step-timeline.js +375 -0
- package/lefthook.yml +33 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +481 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/build-web.ts +35 -1
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/validate-system-prompt.ts +142 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/p2p-chat-tools.ts +6 -6
- package/src/agents/permission-mode.ts +127 -0
- package/src/agents/pi-sdk.ts +741 -30
- package/src/agents/pre-tool-validator.ts +213 -0
- package/src/agents/workflow-pivot-loop.ts +110 -19
- package/src/bollharness/CLAUDE.md +1 -1
- package/src/bollharness/README.md +2 -2
- package/src/bollharness/README.zh-CN.md +2 -2
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
- package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
- package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
- package/src/bollharness/src/scripts/context_router.ts +1 -1
- package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
- package/src/bollharness/src/scripts/guard_router.ts +1 -1
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
- package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
- package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
- package/src/bollharness-integration/context-router.ts +2 -2
- package/src/bollharness-integration/guard-checker.ts +1 -1
- package/src/bootstrap/bootstrap.ts +135 -0
- package/src/bootstrap/context-collector.ts +371 -0
- package/src/bootstrap/context-hierarchy.ts +283 -0
- package/src/bootstrap/lifecycle-hooks.ts +289 -0
- package/src/bootstrap/project-context.ts +171 -0
- package/src/context-compaction/auto-compact.ts +153 -0
- package/src/context-compaction/budget-gate.ts +32 -0
- package/src/context-compaction/budget-reduce.ts +37 -0
- package/src/context-compaction/context-collapse.ts +72 -0
- package/src/context-compaction/index.ts +24 -0
- package/src/context-compaction/microcompact.ts +54 -0
- package/src/context-compaction/pipeline.ts +133 -0
- package/src/context-compaction/snip.ts +51 -0
- package/src/context-compaction/token-estimator.ts +36 -0
- package/src/context-compaction/types.ts +99 -0
- package/src/heartbeat/HealthMonitor.ts +3 -2
- package/src/index.ts +47 -2
- package/src/llm/llm-judgment-client.ts +36 -35
- package/src/llm/pi-ai.ts +135 -29
- package/src/llm/system-prompt/health.ts +159 -0
- package/src/llm/system-prompt/layers/channel/local.md +14 -0
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
- package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
- package/src/llm/system-prompt/layers/core/identity.md +37 -0
- package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
- package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
- package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
- package/src/llm/system-prompt/layers/core/refusal.md +37 -0
- package/src/llm/system-prompt/layers/core/tone.md +31 -0
- package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
- package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
- package/src/llm/system-prompt/layers/role/architect.md +20 -0
- package/src/llm/system-prompt/layers/role/expert.md +19 -0
- package/src/llm/system-prompt/layers/role/implementer.md +15 -0
- package/src/llm/system-prompt/layers/role/security.md +15 -0
- package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
- package/src/llm/system-prompt/layers/tool/bash.md +25 -0
- package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
- package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
- package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
- package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
- package/src/llm/system-prompt/registry.ts +325 -0
- package/src/llm/system-prompt/strip-hibsml.ts +52 -0
- package/src/llm/tool-manifest/ask_user_input.ts +37 -0
- package/src/llm/tool-manifest/bash.ts +25 -0
- package/src/llm/tool-manifest/create_file.ts +26 -0
- package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
- package/src/llm/tool-manifest/image_search.ts +26 -0
- package/src/llm/tool-manifest/index.ts +88 -0
- package/src/llm/tool-manifest/mcp.ts +46 -0
- package/src/llm/tool-manifest/message_compose.ts +33 -0
- package/src/llm/tool-manifest/places.ts +86 -0
- package/src/llm/tool-manifest/present_files.ts +23 -0
- package/src/llm/tool-manifest/recipe.ts +42 -0
- package/src/llm/tool-manifest/recommend_apps.ts +25 -0
- package/src/llm/tool-manifest/str_replace.ts +29 -0
- package/src/llm/tool-manifest/types.ts +52 -0
- package/src/llm/tool-manifest/view.ts +26 -0
- package/src/llm/tool-manifest/weather.ts +29 -0
- package/src/llm/tool-manifest/web.ts +54 -0
- package/src/network/p2p-direct.ts +22 -0
- package/src/network/source-intent-broadcaster.ts +242 -0
- package/src/network/source-intent.ts +167 -0
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/input-scanner.ts +287 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +105 -2187
- package/src/web/client.ts +4326 -0
- package/src/web/index.html +60 -49
- package/src/web/server.ts +894 -148
- package/src/web/style.css +531 -249
- package/src/web/ui/message-renderer.ts +540 -0
- package/src/web/ui/step-timeline.ts +394 -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/test-results/.last-run.json +6 -0
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
|
@@ -0,0 +1,312 @@
|
|
|
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 collectHierarchy(cwd) {
|
|
61
|
+
// 动态 import 避免循环依赖
|
|
62
|
+
const { collectHierarchyLayers, mergeHierarchyLayers } = await import('./context-hierarchy.js');
|
|
63
|
+
try {
|
|
64
|
+
const layers = await collectHierarchyLayers({ cwd });
|
|
65
|
+
const merged = mergeHierarchyLayers(layers, { maxChars: DEFAULT_HIERARCHY_MERGED_MAX });
|
|
66
|
+
return { ...layers, merged };
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn('[context-collector] collectHierarchy failed (silent):', err);
|
|
70
|
+
return { managed: null, user: null, project: null, local: null, merged: '' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const DEFAULT_HIERARCHY_MERGED_MAX = 2000; // 4000 → 2000 (P-Action 4), 跟 context-hierarchy.ts DEFAULT_MERGE_MAX_CHARS 对齐
|
|
74
|
+
async function collectGit(cwd, limit) {
|
|
75
|
+
const branchOut = await safeExecFile('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
76
|
+
if (!branchOut)
|
|
77
|
+
return null;
|
|
78
|
+
const logOut = await safeExecFile('git', ['log', '--oneline', `-n`, String(limit)], cwd);
|
|
79
|
+
const statusOut = await safeExecFile('git', ['status', '--porcelain'], cwd);
|
|
80
|
+
return {
|
|
81
|
+
branch: branchOut,
|
|
82
|
+
lastCommits: logOut ? logOut.split('\n').filter(Boolean) : [],
|
|
83
|
+
uncommittedChanges: statusOut ? statusOut.split('\n').filter(Boolean).length : 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function collectPersona() {
|
|
87
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
88
|
+
const raw = await safeReadFile(path.join(home, '.bolloon', 'persona.json'), 5000);
|
|
89
|
+
if (!raw)
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
const p = JSON.parse(raw);
|
|
93
|
+
return {
|
|
94
|
+
name: String(p.name || 'unknown'),
|
|
95
|
+
description: String(p.description || ''),
|
|
96
|
+
personality: String(p.personality || ''),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function collectJudgmentsSummary(topN) {
|
|
104
|
+
// 用动态 import 避免循环依赖
|
|
105
|
+
const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
106
|
+
let all = [];
|
|
107
|
+
try {
|
|
108
|
+
all = await loadAllJudgments();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { total: 0, active: 0, superseded: 0, rejected: 0, topValues: [] };
|
|
112
|
+
}
|
|
113
|
+
const byStatus = { active: 0, superseded: 0, rejected: 0 };
|
|
114
|
+
for (const j of all) {
|
|
115
|
+
const s = (j.status ?? 'active');
|
|
116
|
+
if (s in byStatus)
|
|
117
|
+
byStatus[s]++;
|
|
118
|
+
}
|
|
119
|
+
// 借用 getRelevantValues 算 top values (已经在 store 里)
|
|
120
|
+
let topValues = [];
|
|
121
|
+
try {
|
|
122
|
+
const { getRelevantValues } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
123
|
+
const values = await getRelevantValues('安全 代码 质量 测试 文档 用户'); // 通用 query
|
|
124
|
+
topValues = values.slice(0, topN).map((v) => ({
|
|
125
|
+
category: v.category,
|
|
126
|
+
value: v.value,
|
|
127
|
+
weight: v.weight,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
catch { /* ignore */ }
|
|
131
|
+
return {
|
|
132
|
+
total: all.length,
|
|
133
|
+
active: byStatus.active,
|
|
134
|
+
superseded: byStatus.superseded,
|
|
135
|
+
rejected: byStatus.rejected,
|
|
136
|
+
topValues,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function collectSkills() {
|
|
140
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
141
|
+
const userSkillsDir = path.join(home, '.bolloon', 'skills');
|
|
142
|
+
const projectSkillsDir = path.join(process.cwd(), '.bolloon', 'skills');
|
|
143
|
+
const out = [];
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (const dir of [userSkillsDir, projectSkillsDir]) {
|
|
146
|
+
try {
|
|
147
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
148
|
+
for (const e of entries) {
|
|
149
|
+
if (!e.isDirectory() || seen.has(e.name))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(e.name);
|
|
152
|
+
// 尝试读 SKILL.md 第一行作为描述
|
|
153
|
+
let description = '';
|
|
154
|
+
try {
|
|
155
|
+
const skillMd = await fs.readFile(path.join(dir, e.name, 'SKILL.md'), 'utf-8');
|
|
156
|
+
// 抓第一段非空非 frontmatter
|
|
157
|
+
const lines = skillMd.split('\n');
|
|
158
|
+
let inFrontmatter = false;
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
if (line.trim() === '---') {
|
|
161
|
+
inFrontmatter = !inFrontmatter;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (inFrontmatter)
|
|
165
|
+
continue;
|
|
166
|
+
if (line.trim() && !line.startsWith('#')) {
|
|
167
|
+
description = line.trim().substring(0, 120);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch { /* ignore */ }
|
|
173
|
+
out.push({ name: e.name, description });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { /* dir 不存在正常 */ }
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
function collectEnv() {
|
|
181
|
+
let llmProvider = 'unknown';
|
|
182
|
+
try {
|
|
183
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
184
|
+
const cfg = require(path.join(home, '.bolloon', 'llm-config.json'));
|
|
185
|
+
if (cfg && typeof cfg === 'object' && 'provider' in cfg) {
|
|
186
|
+
llmProvider = String(cfg.provider);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch { /* ignore */ }
|
|
190
|
+
return {
|
|
191
|
+
os: `${os.platform()} ${os.release()}`,
|
|
192
|
+
nodeVersion: process.version,
|
|
193
|
+
llmProvider,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function collectPending(opts) {
|
|
197
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
198
|
+
const goalsDir = path.join(home, '.bolloon', 'goals');
|
|
199
|
+
const goals = [];
|
|
200
|
+
try {
|
|
201
|
+
const entries = await fs.readdir(goalsDir, { withFileTypes: true });
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
if (e.isFile())
|
|
204
|
+
goals.push(e.name);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch { /* dir 不存在 */ }
|
|
208
|
+
// 扫 TODO/FIXME (限制 20 条避免太长)
|
|
209
|
+
const todoScanDir = opts.todoScanDir ?? path.join(opts.cwd, 'src');
|
|
210
|
+
const todos = await scanTodos(todoScanDir, 20);
|
|
211
|
+
return { goals, todos };
|
|
212
|
+
}
|
|
213
|
+
async function scanTodos(dir, limit) {
|
|
214
|
+
const out = [];
|
|
215
|
+
try {
|
|
216
|
+
await walkDir(dir, async (filePath) => {
|
|
217
|
+
if (!/\.(ts|js|tsx|jsx)$/.test(filePath))
|
|
218
|
+
return;
|
|
219
|
+
// 跳过 dist / node_modules
|
|
220
|
+
if (filePath.includes('/node_modules/') || filePath.includes('/dist/'))
|
|
221
|
+
return;
|
|
222
|
+
try {
|
|
223
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
224
|
+
const lines = content.split('\n');
|
|
225
|
+
for (let i = 0; i < lines.length; i++) {
|
|
226
|
+
const line = lines[i];
|
|
227
|
+
// 匹配 TODO / FIXME / XXX 注释
|
|
228
|
+
const m = line.match(/(?:\/\/|\/\*|\*|<!--)\s*(TODO|FIXME|XXX|HACK)[::]?\s*(.+)/);
|
|
229
|
+
if (m) {
|
|
230
|
+
out.push({
|
|
231
|
+
file: path.relative(dir, filePath),
|
|
232
|
+
line: i + 1,
|
|
233
|
+
text: m[2].trim().substring(0, 80),
|
|
234
|
+
});
|
|
235
|
+
if (out.length >= limit)
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch { /* ignore */ }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch { /* dir 不存在 */ }
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
async function walkDir(dir, cb) {
|
|
247
|
+
let entries;
|
|
248
|
+
try {
|
|
249
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (const e of entries) {
|
|
255
|
+
const full = path.join(dir, e.name);
|
|
256
|
+
if (e.isDirectory()) {
|
|
257
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.'))
|
|
258
|
+
continue;
|
|
259
|
+
await walkDir(full, cb);
|
|
260
|
+
}
|
|
261
|
+
else if (e.isFile()) {
|
|
262
|
+
await cb(full);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// ============================================================
|
|
267
|
+
// 主入口
|
|
268
|
+
// ============================================================
|
|
269
|
+
export async function collectBolloonContext(opts) {
|
|
270
|
+
const { cwd, bolloonMdMaxBytes = 2000, gitCommitLimit = 5, topValuesLimit = 10, } = opts;
|
|
271
|
+
// 并行收集 (除 judgments 因为要动态 import + 依赖其它)
|
|
272
|
+
const [project, git, persona, skills, env, pending, hierarchy] = await Promise.all([
|
|
273
|
+
collectProject(cwd, bolloonMdMaxBytes),
|
|
274
|
+
collectGit(cwd, gitCommitLimit),
|
|
275
|
+
collectPersona(),
|
|
276
|
+
collectSkills(),
|
|
277
|
+
Promise.resolve(collectEnv()),
|
|
278
|
+
collectPending(opts),
|
|
279
|
+
collectHierarchy(cwd),
|
|
280
|
+
]);
|
|
281
|
+
// judgments 单独调 (内部 import)
|
|
282
|
+
const judgmentsSummary = await collectJudgmentsSummary(topValuesLimit);
|
|
283
|
+
return {
|
|
284
|
+
projectRoot: cwd,
|
|
285
|
+
projectName: project.projectName,
|
|
286
|
+
bolloonMd: project.bolloonMd,
|
|
287
|
+
hierarchy,
|
|
288
|
+
git,
|
|
289
|
+
persona,
|
|
290
|
+
judgmentsSummary,
|
|
291
|
+
skills,
|
|
292
|
+
env,
|
|
293
|
+
pending,
|
|
294
|
+
collectedAt: new Date().toISOString(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// ============================================================
|
|
298
|
+
// 24h 缓存 (跟类 B 一致)
|
|
299
|
+
// ============================================================
|
|
300
|
+
let cached = null;
|
|
301
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
302
|
+
export async function getCachedBolloonContext(opts, force = false) {
|
|
303
|
+
if (!force && cached && cached.cwd === opts.cwd && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
304
|
+
return cached.ctx;
|
|
305
|
+
}
|
|
306
|
+
const ctx = await collectBolloonContext(opts);
|
|
307
|
+
cached = { at: Date.now(), ctx, cwd: opts.cwd };
|
|
308
|
+
return ctx;
|
|
309
|
+
}
|
|
310
|
+
export function clearBolloonContextCache() {
|
|
311
|
+
cached = null;
|
|
312
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Hierarchy — 4 级 Bolloon.md 查找 + 合并优先级
|
|
3
|
+
*
|
|
4
|
+
* 设计: 严格对齐 Claude Code 论文 4 级层次
|
|
5
|
+
* 1. Managed — /etc/bolloon/Bolloon.md (企业 IT 部署, 系统级)
|
|
6
|
+
* 2. User — ~/.bolloon/Bolloon.md (用户级, 跨项目)
|
|
7
|
+
* 3. Project — <cwd>/Bolloon.md 或 .claude/rules/*.md
|
|
8
|
+
* 4. Local — <cwd>/CLAUDE.local.md (个人覆盖, .gitignore)
|
|
9
|
+
*
|
|
10
|
+
* 注入约定 (论文):
|
|
11
|
+
* - 拼到 system prompt 顶部 (作为 user context, 概率性遵守)
|
|
12
|
+
* - 不混入 user 消息 (避免 prompt injection)
|
|
13
|
+
* - 截断时反向砍 local → project → user → managed (优先保 managed, 因为是 bolloon 自身约束)
|
|
14
|
+
*
|
|
15
|
+
* 兼容:
|
|
16
|
+
* - 同时识别 Bolloon.md 和 Bolloon.md (论文用前者, bolloon 历史用后者)
|
|
17
|
+
* - 文件名按优先级: Bolloon.md > Bolloon.md
|
|
18
|
+
*
|
|
19
|
+
* 失败静默: 任何 IO 错误 → 返回 null, 不阻塞主流程.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs/promises';
|
|
22
|
+
import * as os from 'os';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
export const DEFAULT_PATHS = {
|
|
25
|
+
managed: '/etc/bolloon/Bolloon.md',
|
|
26
|
+
user: '', // 用 resolveUserPath 填
|
|
27
|
+
project: '', // 用 resolveProjectPaths 填
|
|
28
|
+
projectRulesDir: '',
|
|
29
|
+
local: '',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 单层字符上限 (P-Action 4 收紧, 阶段 0 把 4 级层次控制在 ≤ 2KB).
|
|
33
|
+
* 反向截断策略保证 managed 优先, 单层超限由 truncate() 截断.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_MAX_CHARS = {
|
|
36
|
+
managed: 700, // 1500 → 700, 保 bolloon 自身约束
|
|
37
|
+
user: 500, // 1500 → 500, 跨项目偏好
|
|
38
|
+
project: 500, // 2500 → 500, 项目规则
|
|
39
|
+
local: 300, // 1500 → 300, 个人覆盖
|
|
40
|
+
};
|
|
41
|
+
// ============================================================
|
|
42
|
+
// 路径解析
|
|
43
|
+
// ============================================================
|
|
44
|
+
export function resolveUserPath(home) {
|
|
45
|
+
const h = home ?? process.env.HOME ?? os.homedir() ?? '/tmp';
|
|
46
|
+
return path.join(h, '.bolloon', 'Bolloon.md');
|
|
47
|
+
}
|
|
48
|
+
export function resolveProjectPaths(cwd) {
|
|
49
|
+
return {
|
|
50
|
+
project: path.join(cwd, 'Bolloon.md'),
|
|
51
|
+
projectRulesDir: path.join(cwd, '.claude', 'rules'),
|
|
52
|
+
local: path.join(cwd, 'CLAUDE.local.md'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ============================================================
|
|
56
|
+
// 单层读取 (优先 Bolloon.md, fallback Bolloon.md)
|
|
57
|
+
// ============================================================
|
|
58
|
+
/**
|
|
59
|
+
* 读 1 个文件路径, 失败/缺失返回 null.
|
|
60
|
+
* 字符上限由调用方截断 (这里只负责读全文).
|
|
61
|
+
*/
|
|
62
|
+
async function readRuleFile(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
65
|
+
return content.length > 0 ? content : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 读 project 层: 优先 Bolloon.md, fallback Bolloon.md, 最后尝试 .claude/rules/*.md
|
|
73
|
+
*/
|
|
74
|
+
async function readProjectLayer(projectPath, projectRulesDir, maxChars) {
|
|
75
|
+
// 1. Bolloon.md
|
|
76
|
+
let content = await readRuleFile(projectPath);
|
|
77
|
+
if (content)
|
|
78
|
+
return truncate(content, maxChars);
|
|
79
|
+
// 2. Bolloon.md (向后兼容)
|
|
80
|
+
const bolloonMdPath = path.join(path.dirname(projectPath), 'Bolloon.md');
|
|
81
|
+
content = await readRuleFile(bolloonMdPath);
|
|
82
|
+
if (content)
|
|
83
|
+
return truncate(content, maxChars);
|
|
84
|
+
// 3. .claude/rules/*.md (合并所有)
|
|
85
|
+
try {
|
|
86
|
+
const entries = await fs.readdir(projectRulesDir, { withFileTypes: true });
|
|
87
|
+
const ruleFiles = entries
|
|
88
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
89
|
+
.map((e) => path.join(projectRulesDir, e.name))
|
|
90
|
+
.sort();
|
|
91
|
+
if (ruleFiles.length > 0) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const f of ruleFiles) {
|
|
95
|
+
const c = await readRuleFile(f);
|
|
96
|
+
if (c) {
|
|
97
|
+
parts.push(`### ${path.basename(f, '.md')}\n${c}`);
|
|
98
|
+
total += c.length;
|
|
99
|
+
if (total >= maxChars)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (parts.length > 0)
|
|
104
|
+
return truncate(parts.join('\n\n'), maxChars);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { /* dir 不存在 */ }
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 并行读 4 层, 任何一层失败 = null (不阻塞其他).
|
|
112
|
+
*/
|
|
113
|
+
export async function collectHierarchyLayers(opts) {
|
|
114
|
+
const limits = { ...DEFAULT_MAX_CHARS, ...(opts.limits?.maxChars ?? {}) };
|
|
115
|
+
const home = opts.home ?? process.env.HOME ?? os.homedir() ?? '/tmp';
|
|
116
|
+
const managedPath = opts.limits?.paths?.managed ?? DEFAULT_PATHS.managed;
|
|
117
|
+
const userPath = opts.limits?.paths?.user ?? resolveUserPath(home);
|
|
118
|
+
const projectPath = opts.limits?.paths?.project ?? path.join(opts.cwd, 'Bolloon.md');
|
|
119
|
+
const projectRulesDir = opts.limits?.paths?.projectRulesDir ?? path.join(opts.cwd, '.claude', 'rules');
|
|
120
|
+
const localPath = opts.limits?.paths?.local ?? path.join(opts.cwd, 'CLAUDE.local.md');
|
|
121
|
+
const [managed, user, project, local] = await Promise.all([
|
|
122
|
+
readRuleFile(managedPath).then((c) => c ? truncate(c, limits.managed) : null),
|
|
123
|
+
readRuleFile(userPath).then((c) => c ? truncate(c, limits.user) : null),
|
|
124
|
+
readProjectLayer(projectPath, projectRulesDir, limits.project),
|
|
125
|
+
readRuleFile(localPath).then((c) => c ? truncate(c, limits.local) : null),
|
|
126
|
+
]);
|
|
127
|
+
return { managed, user, project, local };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 4 级合并总字符上限 (P-Action 4 收紧, ≤ 2KB ≈ 500 tokens).
|
|
131
|
+
* 反向截断 (local → project → user → managed) 保证 managed 不丢.
|
|
132
|
+
*/
|
|
133
|
+
export const DEFAULT_MERGE_MAX_CHARS = 2000;
|
|
134
|
+
/**
|
|
135
|
+
* 按 4 级顺序 (managed → user → project → local) 拼接为 markdown 片段.
|
|
136
|
+
*
|
|
137
|
+
* 截断策略: 反向砍 (local → project → user → managed), 优先保 managed.
|
|
138
|
+
* - 总长 < maxChars: 不截断
|
|
139
|
+
* - 总长 ≥ maxChars: 按 (local, project, user) 顺序逐步砍到 firstParagraphs=1
|
|
140
|
+
* - 最后只保 managed (bolloon 自身约束不能丢)
|
|
141
|
+
*/
|
|
142
|
+
export function mergeHierarchyLayers(layers, opts = {}) {
|
|
143
|
+
const maxChars = opts.maxChars ?? DEFAULT_MERGE_MAX_CHARS;
|
|
144
|
+
const parts = [];
|
|
145
|
+
const labels = [
|
|
146
|
+
['managed', '管理规则 (Managed)'],
|
|
147
|
+
['user', '用户规则 (User)'],
|
|
148
|
+
['project', '项目规则 (Project)'],
|
|
149
|
+
['local', '本地规则 (Local)'],
|
|
150
|
+
];
|
|
151
|
+
for (const [key, label] of labels) {
|
|
152
|
+
const content = layers[key];
|
|
153
|
+
if (content) {
|
|
154
|
+
parts.push(`## ${label}\n\n${content}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (parts.length === 0)
|
|
158
|
+
return '';
|
|
159
|
+
let result = parts.join('\n\n---\n\n');
|
|
160
|
+
if (result.length <= maxChars)
|
|
161
|
+
return result;
|
|
162
|
+
// 超限: 反向砍 (local → project → user), 二级回退到 firstParagraphs=1
|
|
163
|
+
return truncateMerged(layers, maxChars);
|
|
164
|
+
}
|
|
165
|
+
function truncateMerged(layers, maxChars) {
|
|
166
|
+
const order = [
|
|
167
|
+
['local', '本地规则 (Local)'],
|
|
168
|
+
['project', '项目规则 (Project)'],
|
|
169
|
+
['user', '用户规则 (User)'],
|
|
170
|
+
['managed', '管理规则 (Managed)'],
|
|
171
|
+
];
|
|
172
|
+
const capped = { ...layers };
|
|
173
|
+
// 反复压缩, 直到总长 ≤ maxChars
|
|
174
|
+
let depth = 0; // 0=原始, 1=firstParagraphs=1, 2=砍半
|
|
175
|
+
while (depth < 3) {
|
|
176
|
+
const parts = [];
|
|
177
|
+
for (const [key, label] of order) {
|
|
178
|
+
const c = capped[key];
|
|
179
|
+
if (!c)
|
|
180
|
+
continue;
|
|
181
|
+
let text = c;
|
|
182
|
+
if (depth === 1)
|
|
183
|
+
text = firstParagraphs(c, 1);
|
|
184
|
+
if (depth === 2)
|
|
185
|
+
text = firstParagraphs(c, 1, Math.floor(maxChars / 4));
|
|
186
|
+
parts.push(`## ${label}\n\n${text}`);
|
|
187
|
+
}
|
|
188
|
+
const joined = parts.reverse().join('\n\n---\n\n'); // 恢复 managed → user → project → local
|
|
189
|
+
if (joined.length <= maxChars)
|
|
190
|
+
return joined;
|
|
191
|
+
depth++;
|
|
192
|
+
}
|
|
193
|
+
// 最后保 managed 一行
|
|
194
|
+
return `## 管理规则 (Managed)\n\n${firstParagraphs(layers.managed ?? '(无)', 1, maxChars - 50)}`;
|
|
195
|
+
}
|
|
196
|
+
// ============================================================
|
|
197
|
+
// 工具
|
|
198
|
+
// ============================================================
|
|
199
|
+
function truncate(s, maxChars) {
|
|
200
|
+
if (s.length <= maxChars)
|
|
201
|
+
return s;
|
|
202
|
+
return s.substring(0, maxChars) + '\n... (truncated)';
|
|
203
|
+
}
|
|
204
|
+
function firstParagraphs(text, count, maxLen) {
|
|
205
|
+
const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
|
|
206
|
+
let result = paragraphs.slice(0, count).join('\n\n');
|
|
207
|
+
if (maxLen && result.length > maxLen) {
|
|
208
|
+
result = result.substring(0, maxLen) + '...';
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
// ============================================================
|
|
213
|
+
// 测试钩子
|
|
214
|
+
// ============================================================
|
|
215
|
+
/** 重置模块级状态 (供测试). 当前无状态, 保留为占位. */
|
|
216
|
+
export function _resetHierarchyForTest() {
|
|
217
|
+
// no-op (collect 是纯函数, 但保留 API 一致)
|
|
218
|
+
}
|