@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,135 @@
|
|
|
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
|
+
|
|
12
|
+
import { runAdaptiveScan, logEvolution } from '../pi-ecosystem-judgment/adaptive-scan.js';
|
|
13
|
+
import { collectBolloonContext, type BolloonContext } from './context-collector.js';
|
|
14
|
+
import type { AdaptiveScanResult } from '../pi-ecosystem-judgment/adaptive-scan.js';
|
|
15
|
+
|
|
16
|
+
export interface BootstrapResult {
|
|
17
|
+
context: BolloonContext;
|
|
18
|
+
scanResult: AdaptiveScanResult;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
// 失败的部分不影响主流程
|
|
21
|
+
errors: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 入口: web server / CLI 启动时调一次
|
|
26
|
+
*/
|
|
27
|
+
export async function bootstrapBolloon(opts: { cwd?: string } = {}): Promise<BootstrapResult> {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
const errors: string[] = [];
|
|
30
|
+
|
|
31
|
+
// 1. 类 B 启动扫描
|
|
32
|
+
let scanResult: AdaptiveScanResult = {
|
|
33
|
+
scannedAt: new Date().toISOString(),
|
|
34
|
+
judgmentsTotal: 0,
|
|
35
|
+
usageEntriesScanned: 0,
|
|
36
|
+
suggestions: [],
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
scanResult = await runAdaptiveScan();
|
|
40
|
+
const { suggestionHint } = await import('../pi-ecosystem-judgment/adaptive-scan.js');
|
|
41
|
+
await logEvolution({
|
|
42
|
+
ts: new Date().toISOString(),
|
|
43
|
+
action: 'accept', // 用 accept 表示"系统记录" (跟 reject 区分)
|
|
44
|
+
suggestion: {
|
|
45
|
+
key: 'bootstrap-startup',
|
|
46
|
+
kind: 'unused', // 占位
|
|
47
|
+
judgmentId: '__bootstrap__',
|
|
48
|
+
decision: 'Bolloon 启动扫描',
|
|
49
|
+
reason: `本次启动扫描了 ${scanResult.judgmentsTotal} 条原则, ${scanResult.usageEntriesScanned} 条使用记录, 生成 ${scanResult.suggestions.length} 条建议`,
|
|
50
|
+
action: 'review',
|
|
51
|
+
hint: suggestionHint('unused', 'review', { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 }),
|
|
52
|
+
metrics: { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 },
|
|
53
|
+
scannedAt: scanResult.scannedAt,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
console.log(`[bootstrap] 类 B 启动扫描完成: ${scanResult.suggestions.length} 条建议`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
errors.push(`scan: ${(err as Error).message}`);
|
|
59
|
+
console.warn('[bootstrap] 启动扫描失败 (非致命):', err);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. 收集项目 Context
|
|
63
|
+
let context: BolloonContext = {
|
|
64
|
+
projectRoot: opts.cwd ?? process.cwd(),
|
|
65
|
+
projectName: 'unknown',
|
|
66
|
+
bolloonMd: null,
|
|
67
|
+
hierarchy: { managed: null, user: null, project: null, local: null, merged: '' },
|
|
68
|
+
git: null,
|
|
69
|
+
persona: null,
|
|
70
|
+
judgmentsSummary: { total: 0, active: 0, superseded: 0, rejected: 0, topValues: [] },
|
|
71
|
+
skills: [],
|
|
72
|
+
env: { os: 'unknown', nodeVersion: 'unknown', llmProvider: 'unknown' },
|
|
73
|
+
pending: { goals: [], todos: [] },
|
|
74
|
+
collectedAt: new Date().toISOString(),
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
context = await collectBolloonContext({ cwd: opts.cwd ?? process.cwd() });
|
|
78
|
+
console.log(`[bootstrap] context 收集完成: ${context.judgmentsSummary.total} judgments, ${context.skills.length} skills`);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
errors.push(`context: ${(err as Error).message}`);
|
|
81
|
+
console.warn('[bootstrap] context 收集失败 (非致命):', err);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. 挂定时任务 (每天 0:00 跑扫描, server 重启时丢失可接受)
|
|
85
|
+
try {
|
|
86
|
+
scheduleAdaptiveScanDaily();
|
|
87
|
+
console.log('[bootstrap] 定时任务已挂: 每天 0:00 跑类 B 扫描');
|
|
88
|
+
} catch (err) {
|
|
89
|
+
errors.push(`schedule: ${(err as Error).message}`);
|
|
90
|
+
console.warn('[bootstrap] 定时任务挂载失败 (非致命):', err);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const durationMs = Date.now() - start;
|
|
94
|
+
console.log(`[bootstrap] 完成 (${durationMs}ms, ${errors.length} 个错误)`);
|
|
95
|
+
|
|
96
|
+
return { context, scanResult, durationMs, errors };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================
|
|
100
|
+
// 定时任务: 每天 0:00 跑类 B 自适应扫描
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
let scheduled = false;
|
|
104
|
+
|
|
105
|
+
function scheduleAdaptiveScanDaily(): void {
|
|
106
|
+
if (scheduled) return;
|
|
107
|
+
scheduled = true;
|
|
108
|
+
|
|
109
|
+
const now = new Date();
|
|
110
|
+
const next = new Date(now);
|
|
111
|
+
next.setHours(24, 0, 0, 0);
|
|
112
|
+
const msUntilMidnight = next.getTime() - now.getTime();
|
|
113
|
+
|
|
114
|
+
// 第一次: 等到明天 0:00
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
runAdaptiveScan().then((result) => {
|
|
117
|
+
console.log(`[bootstrap] 定时扫描完成: ${result.suggestions.length} 条建议`);
|
|
118
|
+
}).catch((err) => {
|
|
119
|
+
console.warn('[bootstrap] 定时扫描失败:', err);
|
|
120
|
+
});
|
|
121
|
+
// 之后: 每 24h
|
|
122
|
+
setInterval(() => {
|
|
123
|
+
runAdaptiveScan().then((result) => {
|
|
124
|
+
console.log(`[bootstrap] 定时扫描完成: ${result.suggestions.length} 条建议`);
|
|
125
|
+
}).catch((err) => {
|
|
126
|
+
console.warn('[bootstrap] 定时扫描失败:', err);
|
|
127
|
+
});
|
|
128
|
+
}, 24 * 60 * 60 * 1000);
|
|
129
|
+
}, msUntilMidnight);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 测试辅助: 重置 scheduled 标志 */
|
|
133
|
+
export function _resetScheduleForTest(): void {
|
|
134
|
+
scheduled = false;
|
|
135
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
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
|
+
|
|
19
|
+
import * as fs from 'fs/promises';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { execFile } from 'child_process';
|
|
23
|
+
import { promisify } from 'util';
|
|
24
|
+
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
27
|
+
export interface BolloonContext {
|
|
28
|
+
// 1. 项目层
|
|
29
|
+
projectRoot: string;
|
|
30
|
+
projectName: string;
|
|
31
|
+
bolloonMd: string | null;
|
|
32
|
+
// 1b. Bolloon.md 4 级层次 (Claude Code 论文对齐, 严格 1:1)
|
|
33
|
+
// managed: /etc/bolloon/Bolloon.md (企业 IT 部署)
|
|
34
|
+
// user: ~/.bolloon/Bolloon.md
|
|
35
|
+
// project: <cwd>/Bolloon.md (Bolloon.md 兼容)
|
|
36
|
+
// local: <cwd>/CLAUDE.local.md
|
|
37
|
+
hierarchy: {
|
|
38
|
+
managed: string | null;
|
|
39
|
+
user: string | null;
|
|
40
|
+
project: string | null;
|
|
41
|
+
local: string | null;
|
|
42
|
+
merged: string; // 已按优先级合并的 markdown 片段
|
|
43
|
+
};
|
|
44
|
+
// 2. git 层
|
|
45
|
+
git: {
|
|
46
|
+
branch: string;
|
|
47
|
+
lastCommits: string[];
|
|
48
|
+
uncommittedChanges: number;
|
|
49
|
+
} | null;
|
|
50
|
+
// 3. persona
|
|
51
|
+
persona: { name: string; description: string; personality: string } | null;
|
|
52
|
+
// 4. judgments 摘要
|
|
53
|
+
judgmentsSummary: {
|
|
54
|
+
total: number;
|
|
55
|
+
active: number;
|
|
56
|
+
superseded: number;
|
|
57
|
+
rejected: number;
|
|
58
|
+
topValues: Array<{ category: string; value: string; weight: number }>;
|
|
59
|
+
};
|
|
60
|
+
// 5. skills
|
|
61
|
+
skills: Array<{ name: string; description: string }>;
|
|
62
|
+
// 6. 环境
|
|
63
|
+
env: { os: string; nodeVersion: string; llmProvider: string };
|
|
64
|
+
// 7. pending
|
|
65
|
+
pending: {
|
|
66
|
+
goals: string[];
|
|
67
|
+
todos: Array<{ file: string; line: number; text: string }>;
|
|
68
|
+
};
|
|
69
|
+
// 采集时间
|
|
70
|
+
collectedAt: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CollectOptions {
|
|
74
|
+
cwd: string;
|
|
75
|
+
/** Bolloon.md 全文最大字符数 (默认 2000) */
|
|
76
|
+
bolloonMdMaxBytes?: number;
|
|
77
|
+
/** git log 几条 (默认 5) */
|
|
78
|
+
gitCommitLimit?: number;
|
|
79
|
+
/** judgments Top N values (默认 10) */
|
|
80
|
+
topValuesLimit?: number;
|
|
81
|
+
/** 扫 TODO/FIXME 的根目录, 默认 cwd/src */
|
|
82
|
+
todoScanDir?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================
|
|
86
|
+
// 单个收集器: 全部独立, 失败静默
|
|
87
|
+
// ============================================================
|
|
88
|
+
|
|
89
|
+
async function safeReadFile(filePath: string, maxBytes: number): Promise<string | null> {
|
|
90
|
+
try {
|
|
91
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
92
|
+
if (content.length > maxBytes) {
|
|
93
|
+
return content.substring(0, maxBytes) + '\n... (truncated)';
|
|
94
|
+
}
|
|
95
|
+
return content;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function safeExecFile(cmd: string, args: string[], cwd: string): Promise<string> {
|
|
102
|
+
try {
|
|
103
|
+
const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 5000 });
|
|
104
|
+
return stdout.trim();
|
|
105
|
+
} catch {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function collectProject(cwd: string, bolloonMdMaxBytes: number) {
|
|
111
|
+
let projectName = path.basename(cwd);
|
|
112
|
+
try {
|
|
113
|
+
const pkgRaw = await fs.readFile(path.join(cwd, 'package.json'), 'utf-8');
|
|
114
|
+
const pkg = JSON.parse(pkgRaw);
|
|
115
|
+
if (typeof pkg.name === 'string') projectName = pkg.name;
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
const bolloonMd = await safeReadFile(path.join(cwd, 'Bolloon.md'), bolloonMdMaxBytes);
|
|
118
|
+
return { projectName, bolloonMd };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function collectHierarchy(cwd: string): Promise<BolloonContext['hierarchy']> {
|
|
122
|
+
// 动态 import 避免循环依赖
|
|
123
|
+
const { collectHierarchyLayers, mergeHierarchyLayers } = await import('./context-hierarchy.js');
|
|
124
|
+
try {
|
|
125
|
+
const layers = await collectHierarchyLayers({ cwd });
|
|
126
|
+
const merged = mergeHierarchyLayers(layers, { maxChars: DEFAULT_HIERARCHY_MERGED_MAX });
|
|
127
|
+
return { ...layers, merged };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.warn('[context-collector] collectHierarchy failed (silent):', err);
|
|
130
|
+
return { managed: null, user: null, project: null, local: null, merged: '' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const DEFAULT_HIERARCHY_MERGED_MAX = 2000; // 4000 → 2000 (P-Action 4), 跟 context-hierarchy.ts DEFAULT_MERGE_MAX_CHARS 对齐
|
|
135
|
+
|
|
136
|
+
async function collectGit(cwd: string, limit: number): Promise<BolloonContext['git']> {
|
|
137
|
+
const branchOut = await safeExecFile('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
138
|
+
if (!branchOut) return null;
|
|
139
|
+
const logOut = await safeExecFile('git', ['log', '--oneline', `-n`, String(limit)], cwd);
|
|
140
|
+
const statusOut = await safeExecFile('git', ['status', '--porcelain'], cwd);
|
|
141
|
+
return {
|
|
142
|
+
branch: branchOut,
|
|
143
|
+
lastCommits: logOut ? logOut.split('\n').filter(Boolean) : [],
|
|
144
|
+
uncommittedChanges: statusOut ? statusOut.split('\n').filter(Boolean).length : 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function collectPersona(): Promise<BolloonContext['persona']> {
|
|
149
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
150
|
+
const raw = await safeReadFile(path.join(home, '.bolloon', 'persona.json'), 5000);
|
|
151
|
+
if (!raw) return null;
|
|
152
|
+
try {
|
|
153
|
+
const p = JSON.parse(raw);
|
|
154
|
+
return {
|
|
155
|
+
name: String(p.name || 'unknown'),
|
|
156
|
+
description: String(p.description || ''),
|
|
157
|
+
personality: String(p.personality || ''),
|
|
158
|
+
};
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function collectJudgmentsSummary(topN: number): Promise<BolloonContext['judgmentsSummary']> {
|
|
165
|
+
// 用动态 import 避免循环依赖
|
|
166
|
+
const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
167
|
+
let all: Awaited<ReturnType<typeof loadAllJudgments>> = [];
|
|
168
|
+
try {
|
|
169
|
+
all = await loadAllJudgments();
|
|
170
|
+
} catch {
|
|
171
|
+
return { total: 0, active: 0, superseded: 0, rejected: 0, topValues: [] };
|
|
172
|
+
}
|
|
173
|
+
const byStatus = { active: 0, superseded: 0, rejected: 0 };
|
|
174
|
+
for (const j of all) {
|
|
175
|
+
const s = (j.status ?? 'active') as 'active' | 'superseded' | 'rejected';
|
|
176
|
+
if (s in byStatus) byStatus[s]++;
|
|
177
|
+
}
|
|
178
|
+
// 借用 getRelevantValues 算 top values (已经在 store 里)
|
|
179
|
+
let topValues: BolloonContext['judgmentsSummary']['topValues'] = [];
|
|
180
|
+
try {
|
|
181
|
+
const { getRelevantValues } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
182
|
+
const values = await getRelevantValues('安全 代码 质量 测试 文档 用户'); // 通用 query
|
|
183
|
+
topValues = values.slice(0, topN).map((v) => ({
|
|
184
|
+
category: v.category,
|
|
185
|
+
value: v.value,
|
|
186
|
+
weight: v.weight,
|
|
187
|
+
}));
|
|
188
|
+
} catch { /* ignore */ }
|
|
189
|
+
return {
|
|
190
|
+
total: all.length,
|
|
191
|
+
active: byStatus.active,
|
|
192
|
+
superseded: byStatus.superseded,
|
|
193
|
+
rejected: byStatus.rejected,
|
|
194
|
+
topValues,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function collectSkills(): Promise<BolloonContext['skills']> {
|
|
199
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
200
|
+
const userSkillsDir = path.join(home, '.bolloon', 'skills');
|
|
201
|
+
const projectSkillsDir = path.join(process.cwd(), '.bolloon', 'skills');
|
|
202
|
+
const out: BolloonContext['skills'] = [];
|
|
203
|
+
const seen = new Set<string>();
|
|
204
|
+
for (const dir of [userSkillsDir, projectSkillsDir]) {
|
|
205
|
+
try {
|
|
206
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
207
|
+
for (const e of entries) {
|
|
208
|
+
if (!e.isDirectory() || seen.has(e.name)) continue;
|
|
209
|
+
seen.add(e.name);
|
|
210
|
+
// 尝试读 SKILL.md 第一行作为描述
|
|
211
|
+
let description = '';
|
|
212
|
+
try {
|
|
213
|
+
const skillMd = await fs.readFile(path.join(dir, e.name, 'SKILL.md'), 'utf-8');
|
|
214
|
+
// 抓第一段非空非 frontmatter
|
|
215
|
+
const lines = skillMd.split('\n');
|
|
216
|
+
let inFrontmatter = false;
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
if (line.trim() === '---') { inFrontmatter = !inFrontmatter; continue; }
|
|
219
|
+
if (inFrontmatter) continue;
|
|
220
|
+
if (line.trim() && !line.startsWith('#')) {
|
|
221
|
+
description = line.trim().substring(0, 120);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch { /* ignore */ }
|
|
226
|
+
out.push({ name: e.name, description });
|
|
227
|
+
}
|
|
228
|
+
} catch { /* dir 不存在正常 */ }
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function collectEnv(): BolloonContext['env'] {
|
|
234
|
+
let llmProvider = 'unknown';
|
|
235
|
+
try {
|
|
236
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
237
|
+
const cfg = require(path.join(home, '.bolloon', 'llm-config.json'));
|
|
238
|
+
if (cfg && typeof cfg === 'object' && 'provider' in cfg) {
|
|
239
|
+
llmProvider = String(cfg.provider);
|
|
240
|
+
}
|
|
241
|
+
} catch { /* ignore */ }
|
|
242
|
+
return {
|
|
243
|
+
os: `${os.platform()} ${os.release()}`,
|
|
244
|
+
nodeVersion: process.version,
|
|
245
|
+
llmProvider,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function collectPending(opts: CollectOptions): Promise<BolloonContext['pending']> {
|
|
250
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
251
|
+
const goalsDir = path.join(home, '.bolloon', 'goals');
|
|
252
|
+
const goals: string[] = [];
|
|
253
|
+
try {
|
|
254
|
+
const entries = await fs.readdir(goalsDir, { withFileTypes: true });
|
|
255
|
+
for (const e of entries) {
|
|
256
|
+
if (e.isFile()) goals.push(e.name);
|
|
257
|
+
}
|
|
258
|
+
} catch { /* dir 不存在 */ }
|
|
259
|
+
|
|
260
|
+
// 扫 TODO/FIXME (限制 20 条避免太长)
|
|
261
|
+
const todoScanDir = opts.todoScanDir ?? path.join(opts.cwd, 'src');
|
|
262
|
+
const todos = await scanTodos(todoScanDir, 20);
|
|
263
|
+
|
|
264
|
+
return { goals, todos };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function scanTodos(dir: string, limit: number): Promise<BolloonContext['pending']['todos']> {
|
|
268
|
+
const out: BolloonContext['pending']['todos'] = [];
|
|
269
|
+
try {
|
|
270
|
+
await walkDir(dir, async (filePath) => {
|
|
271
|
+
if (!/\.(ts|js|tsx|jsx)$/.test(filePath)) return;
|
|
272
|
+
// 跳过 dist / node_modules
|
|
273
|
+
if (filePath.includes('/node_modules/') || filePath.includes('/dist/')) return;
|
|
274
|
+
try {
|
|
275
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
276
|
+
const lines = content.split('\n');
|
|
277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
278
|
+
const line = lines[i];
|
|
279
|
+
// 匹配 TODO / FIXME / XXX 注释
|
|
280
|
+
const m = line.match(/(?:\/\/|\/\*|\*|<!--)\s*(TODO|FIXME|XXX|HACK)[::]?\s*(.+)/);
|
|
281
|
+
if (m) {
|
|
282
|
+
out.push({
|
|
283
|
+
file: path.relative(dir, filePath),
|
|
284
|
+
line: i + 1,
|
|
285
|
+
text: m[2].trim().substring(0, 80),
|
|
286
|
+
});
|
|
287
|
+
if (out.length >= limit) return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch { /* ignore */ }
|
|
291
|
+
});
|
|
292
|
+
} catch { /* dir 不存在 */ }
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function walkDir(dir: string, cb: (file: string) => Promise<void>): Promise<void> {
|
|
297
|
+
let entries: import('fs').Dirent[];
|
|
298
|
+
try {
|
|
299
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
300
|
+
} catch {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
for (const e of entries) {
|
|
304
|
+
const full = path.join(dir, e.name);
|
|
305
|
+
if (e.isDirectory()) {
|
|
306
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.')) continue;
|
|
307
|
+
await walkDir(full, cb);
|
|
308
|
+
} else if (e.isFile()) {
|
|
309
|
+
await cb(full);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================
|
|
315
|
+
// 主入口
|
|
316
|
+
// ============================================================
|
|
317
|
+
|
|
318
|
+
export async function collectBolloonContext(opts: CollectOptions): Promise<BolloonContext> {
|
|
319
|
+
const {
|
|
320
|
+
cwd,
|
|
321
|
+
bolloonMdMaxBytes = 2000,
|
|
322
|
+
gitCommitLimit = 5,
|
|
323
|
+
topValuesLimit = 10,
|
|
324
|
+
} = opts;
|
|
325
|
+
// 并行收集 (除 judgments 因为要动态 import + 依赖其它)
|
|
326
|
+
const [project, git, persona, skills, env, pending, hierarchy] = await Promise.all([
|
|
327
|
+
collectProject(cwd, bolloonMdMaxBytes),
|
|
328
|
+
collectGit(cwd, gitCommitLimit),
|
|
329
|
+
collectPersona(),
|
|
330
|
+
collectSkills(),
|
|
331
|
+
Promise.resolve(collectEnv()),
|
|
332
|
+
collectPending(opts),
|
|
333
|
+
collectHierarchy(cwd),
|
|
334
|
+
]);
|
|
335
|
+
// judgments 单独调 (内部 import)
|
|
336
|
+
const judgmentsSummary = await collectJudgmentsSummary(topValuesLimit);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
projectRoot: cwd,
|
|
340
|
+
projectName: project.projectName,
|
|
341
|
+
bolloonMd: project.bolloonMd,
|
|
342
|
+
hierarchy,
|
|
343
|
+
git,
|
|
344
|
+
persona,
|
|
345
|
+
judgmentsSummary,
|
|
346
|
+
skills,
|
|
347
|
+
env,
|
|
348
|
+
pending,
|
|
349
|
+
collectedAt: new Date().toISOString(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ============================================================
|
|
354
|
+
// 24h 缓存 (跟类 B 一致)
|
|
355
|
+
// ============================================================
|
|
356
|
+
|
|
357
|
+
let cached: { at: number; ctx: BolloonContext; cwd: string } | null = null;
|
|
358
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
359
|
+
|
|
360
|
+
export async function getCachedBolloonContext(opts: CollectOptions, force: boolean = false): Promise<BolloonContext> {
|
|
361
|
+
if (!force && cached && cached.cwd === opts.cwd && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
362
|
+
return cached.ctx;
|
|
363
|
+
}
|
|
364
|
+
const ctx = await collectBolloonContext(opts);
|
|
365
|
+
cached = { at: Date.now(), ctx, cwd: opts.cwd };
|
|
366
|
+
return ctx;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function clearBolloonContextCache(): void {
|
|
370
|
+
cached = null;
|
|
371
|
+
}
|