ppxans-harness 2.4.0 → 3.2.0
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/LICENSE +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/edit/editblock.js — aider 风格 SEARCH/REPLACE 编辑块
|
|
2
|
+
// 解析 LLM 输出的编辑块, 应用 (精确/去首尾空行/模糊匹配), 生成回灌提示。
|
|
3
|
+
// 纯 Node、零依赖、ESM。
|
|
4
|
+
|
|
5
|
+
import { strict as assert } from "node:assert";
|
|
6
|
+
|
|
7
|
+
// ---- 解析: 容错处理围栏/冒号/反引号/多块 ----
|
|
8
|
+
// 块格式:
|
|
9
|
+
// <<<<<<< SEARCH
|
|
10
|
+
// [path/to/file.js]
|
|
11
|
+
// 旧代码
|
|
12
|
+
// =======
|
|
13
|
+
// 新代码
|
|
14
|
+
// >>>>>>> REPLACE
|
|
15
|
+
export function parseEditBlocks(text) {
|
|
16
|
+
const blocks = [];
|
|
17
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
18
|
+
let state = "seek"; // seek | path | search | replace
|
|
19
|
+
let cur = null;
|
|
20
|
+
|
|
21
|
+
const trimFence = (s) => s.replace(/^`+|`+$/g, "").trim();
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const raw = lines[i];
|
|
25
|
+
const line = trimFence(raw);
|
|
26
|
+
|
|
27
|
+
if (state === "seek") {
|
|
28
|
+
if (/^<{5,}\s*SEARCH/i.test(line)) {
|
|
29
|
+
cur = { path: null, search: "", replace: "" };
|
|
30
|
+
state = "path";
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (state === "path") {
|
|
36
|
+
// 第一行即路径 (可能带 [] 或反引号围栏)
|
|
37
|
+
let p = line.replace(/[\[\]`]/g, "").replace(/^[\s:]+|[\s:]+$/g, "").trim();
|
|
38
|
+
cur.path = p || null;
|
|
39
|
+
state = "search";
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (state === "search") {
|
|
44
|
+
if (/^={5,}/.test(line)) {
|
|
45
|
+
state = "replace";
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (/^>{5,}/.test(line)) {
|
|
49
|
+
// 异常: 缺少 divider, 放弃当前块
|
|
50
|
+
cur = null;
|
|
51
|
+
state = "seek";
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
cur.search += (cur.search ? "\n" : "") + raw;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (state === "replace") {
|
|
59
|
+
if (/^>{5,}/.test(line)) {
|
|
60
|
+
blocks.push(finalize(cur));
|
|
61
|
+
cur = null;
|
|
62
|
+
state = "seek";
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
cur.replace += (cur.replace ? "\n" : "") + raw;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return blocks;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function finalize(block) {
|
|
73
|
+
return { path: block.path, search: block.search, replace: block.replace };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- 工具函数 ----
|
|
77
|
+
function stripEdgeBlanks(s) {
|
|
78
|
+
const lines = String(s).split("\n");
|
|
79
|
+
while (lines.length && lines[0].trim() === "") lines.shift();
|
|
80
|
+
while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function countOccurrences(hay, needle) {
|
|
85
|
+
if (!needle) return 0;
|
|
86
|
+
let c = 0;
|
|
87
|
+
let i = 0;
|
|
88
|
+
while ((i = hay.indexOf(needle, i)) !== -1) {
|
|
89
|
+
c++;
|
|
90
|
+
i += needle.length;
|
|
91
|
+
}
|
|
92
|
+
return c;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shortPreview(s) {
|
|
96
|
+
const one = String(s).replace(/\n/g, " ").slice(0, 40);
|
|
97
|
+
return one + (String(s).length > 40 ? "…" : "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 模糊匹配计划: 按行 trim 后逐行滑动窗口, 统计命中次数并给出替换方案
|
|
101
|
+
function fuzzyPlan(content, search, replace) {
|
|
102
|
+
const cLines = content.split("\n");
|
|
103
|
+
const sLines = search.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
104
|
+
if (sLines.length === 0) return null;
|
|
105
|
+
const n = sLines.length;
|
|
106
|
+
let first = null;
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (let i = 0; i + n <= cLines.length; i++) {
|
|
109
|
+
let ok = true;
|
|
110
|
+
for (let j = 0; j < n; j++) {
|
|
111
|
+
if (cLines[i + j].trim() !== sLines[j]) {
|
|
112
|
+
ok = false;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (ok) {
|
|
117
|
+
count++;
|
|
118
|
+
if (first === null) first = i;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (count === 0) return null;
|
|
122
|
+
return { start: first, n, repLines: replace.split("\n"), count };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function applyFuzzy(content, plan) {
|
|
126
|
+
const cLines = content.split("\n");
|
|
127
|
+
const out = cLines
|
|
128
|
+
.slice(0, plan.start)
|
|
129
|
+
.concat(plan.repLines, cLines.slice(plan.start + plan.n));
|
|
130
|
+
return out.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- 最佳匹配窗口诊断 (2026-10-02 深度优化) ----
|
|
134
|
+
// not-found 时在文件里找"最像 SEARCH 的区域", 给出行号+相似度, 喂回 LLM 精准自修正,
|
|
135
|
+
// 替代原先"未找到+首尾20行"的瞎蒙模式。
|
|
136
|
+
// 逐行比较: 相等=1 分, 一方包含另一方=0.6 分 (容忍空格/标点微差), 空白行跳过。
|
|
137
|
+
function bestMatchWindow(content, search) {
|
|
138
|
+
const cLines = String(content).split("\n");
|
|
139
|
+
const sLines = String(search).split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
140
|
+
if (!sLines.length || !cLines.length) return null;
|
|
141
|
+
const norm = (s) => s.toLowerCase().replace(/\s+/g, " ");
|
|
142
|
+
const sNorm = sLines.map(norm);
|
|
143
|
+
let best = null;
|
|
144
|
+
for (let i = 0; i + sLines.length <= cLines.length; i++) {
|
|
145
|
+
let hit = 0;
|
|
146
|
+
for (let j = 0; j < sLines.length; j++) {
|
|
147
|
+
const a = norm(cLines[i + j]);
|
|
148
|
+
if (!a && !sNorm[j]) { hit += 1; continue; }
|
|
149
|
+
if (a === sNorm[j]) hit += 1;
|
|
150
|
+
else if (a && (a.includes(sNorm[j]) || sNorm[j].includes(a))) hit += 0.6;
|
|
151
|
+
}
|
|
152
|
+
const score = hit / sLines.length;
|
|
153
|
+
if (!best || score > best.score) best = { line: i + 1, score };
|
|
154
|
+
if (best.score === 1) break;
|
|
155
|
+
}
|
|
156
|
+
return best && best.score >= 0.3 ? best : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- 应用单个块 ----
|
|
160
|
+
// 返回 { ok, content, matched, kind, error?, search }
|
|
161
|
+
export function applyEditBlock(content, block, { fuzzy = true } = {}) {
|
|
162
|
+
const search = block.search ?? "";
|
|
163
|
+
const replace = block.replace ?? "";
|
|
164
|
+
const result = { ok: false, content, matched: false, kind: "none", search };
|
|
165
|
+
|
|
166
|
+
if (!search) {
|
|
167
|
+
result.error = "搜索内容为空 (empty)";
|
|
168
|
+
result.kind = "empty";
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const candidates = [{ s: search, r: replace, kind: "exact" }];
|
|
173
|
+
const s2 = stripEdgeBlanks(search);
|
|
174
|
+
if (s2 && s2 !== search) {
|
|
175
|
+
candidates.push({ s: s2, r: stripEdgeBlanks(replace), kind: "edge-trim" });
|
|
176
|
+
}
|
|
177
|
+
if (fuzzy) {
|
|
178
|
+
const f = fuzzyPlan(content, search, replace);
|
|
179
|
+
if (f) candidates.push({ s: null, r: null, kind: "fuzzy", plan: f });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const c of candidates) {
|
|
183
|
+
if (c.kind === "fuzzy") {
|
|
184
|
+
if (c.plan.count > 1) {
|
|
185
|
+
result.kind = "ambiguous";
|
|
186
|
+
result.error = `多处命中 (ambiguous): ${shortPreview(search)}`;
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
result.ok = true;
|
|
190
|
+
result.matched = true;
|
|
191
|
+
result.kind = "fuzzy";
|
|
192
|
+
result.content = applyFuzzy(content, c.plan);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
const count = countOccurrences(content, c.s);
|
|
196
|
+
if (count === 0) continue;
|
|
197
|
+
if (count > 1) {
|
|
198
|
+
result.kind = "ambiguous";
|
|
199
|
+
result.error = `多处命中 (ambiguous): ${shortPreview(c.s)}`;
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
result.ok = true;
|
|
203
|
+
result.matched = true;
|
|
204
|
+
result.kind = c.kind;
|
|
205
|
+
result.content = content.replace(c.s, c.r);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
result.kind = "not-found";
|
|
210
|
+
result.error = `未找到匹配 (not-found): ${shortPreview(search)}`;
|
|
211
|
+
const hint = bestMatchWindow(content, search);
|
|
212
|
+
if (hint) {
|
|
213
|
+
result.hint = hint;
|
|
214
|
+
result.error += ` — 最接近的位置在第 ${hint.line} 行附近 (相似度 ${Math.round(hint.score * 100)}%), 请对照该区域修正 SEARCH 块`;
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- 顺序应用全部块 ----
|
|
220
|
+
export function applyAll(content, blocks) {
|
|
221
|
+
const results = [];
|
|
222
|
+
let cur = content;
|
|
223
|
+
for (const b of blocks) {
|
|
224
|
+
const r = applyEditBlock(cur, b);
|
|
225
|
+
results.push({ path: b.path, ...r });
|
|
226
|
+
if (r.ok) cur = r.content;
|
|
227
|
+
// 失败的块保留原内容继续, 由回灌循环修正
|
|
228
|
+
}
|
|
229
|
+
return { ok: results.every((r) => r.ok), content: cur, results };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- 生成回灌 LLM 的失败块修复提示 (aider 回灌循环) ----
|
|
233
|
+
// 2026-10-02: 失败块附最佳匹配区域摘录 (±5 行), LLM 拿着原文改 SEARCH, 不再瞎蒙
|
|
234
|
+
export function formatRetryFeedback(results, fileContent = "") {
|
|
235
|
+
const failed = (results || []).filter((r) => !r.ok);
|
|
236
|
+
if (failed.length === 0) return "";
|
|
237
|
+
|
|
238
|
+
const out = [];
|
|
239
|
+
out.push("以下编辑块应用失败, 请根据当前文件内容修正后重试 (SEARCH 块必须精确匹配现有内容):");
|
|
240
|
+
const fl = String(fileContent || "").split("\n");
|
|
241
|
+
for (const f of failed) {
|
|
242
|
+
out.push(`- 文件 ${f.path || "(未知)"} 失败类型=${f.kind}: ${f.error || ""}`);
|
|
243
|
+
if (f.search) {
|
|
244
|
+
out.push(" 原 SEARCH 预览: " + shortPreview(f.search));
|
|
245
|
+
}
|
|
246
|
+
// 带上最像 SEARCH 的原文区域 (±5 行), LLM 照着改即可命中
|
|
247
|
+
if (f.hint && fl.length) {
|
|
248
|
+
const lo = Math.max(0, f.hint.line - 6);
|
|
249
|
+
const hi = Math.min(fl.length, f.hint.line - 1 + 5 + 1);
|
|
250
|
+
out.push(` 当前文件第 ${lo + 1}-${hi} 行 (最接近 SEARCH 的区域):`);
|
|
251
|
+
out.push(fl.slice(lo, hi).map((l, i) => ` ${lo + i + 1} | ${l}`).join("\n"));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (fileContent && !failed.some((f) => f.hint)) {
|
|
255
|
+
out.push("== 文件前 20 行 ==");
|
|
256
|
+
out.push(fl.slice(0, 20).join("\n"));
|
|
257
|
+
if (fl.length > 20) {
|
|
258
|
+
out.push("== 文件后 20 行 ==");
|
|
259
|
+
out.push(fl.slice(-20).join("\n"));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return out.join("\n");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 供测试断言一致性 (非公开 API 也导出以便内部复用验证)
|
|
266
|
+
export const __internal = { stripEdgeBlanks, countOccurrences, fuzzyPlan };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/edit/snapshot.js — 编辑快照与回滚
|
|
2
|
+
// 编辑前对目标文件做内存快照, 出错时整批回滚 (恢复修改 / 删除新建 / 重建目录)。
|
|
3
|
+
// 纯 Node、零依赖、ESM。
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export const Snapshot = {
|
|
9
|
+
// 读取给定路径的文件内容入内存
|
|
10
|
+
// 返回 { id, entries: Map<path, {existed, content, mtime}> }
|
|
11
|
+
begin(paths = []) {
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
for (const p of paths) {
|
|
14
|
+
let existed = false;
|
|
15
|
+
let content = "";
|
|
16
|
+
let mtime = 0;
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs.statSync(p);
|
|
19
|
+
if (stat.isFile()) {
|
|
20
|
+
existed = true;
|
|
21
|
+
content = fs.readFileSync(p, "utf8");
|
|
22
|
+
mtime = stat.mtimeMs;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// 文件不存在或不可读 -> existed=false
|
|
26
|
+
}
|
|
27
|
+
entries.set(p, { existed, content, mtime });
|
|
28
|
+
}
|
|
29
|
+
const id =
|
|
30
|
+
"snap-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
|
|
31
|
+
return { id, entries, createdAt: Date.now() };
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// 快照内文件清单
|
|
35
|
+
list(snap) {
|
|
36
|
+
if (!snap || !snap.entries) return [];
|
|
37
|
+
return [...snap.entries.keys()];
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// 把文件恢复到快照状态
|
|
41
|
+
// existed=false: 删除运行期间新建的文件
|
|
42
|
+
// existed=true : 确保目录存在, 先写临时文件再 rename 覆盖 (原子)
|
|
43
|
+
rollback(snap) {
|
|
44
|
+
if (!snap || !snap.entries) return false;
|
|
45
|
+
for (const [p, e] of snap.entries) {
|
|
46
|
+
if (!e.existed) {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
49
|
+
} catch {
|
|
50
|
+
/* 忽略删除失败 */
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
try {
|
|
54
|
+
const dir = path.dirname(p);
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
const tmp =
|
|
57
|
+
p + ".ppx-rollback-" + Math.random().toString(36).slice(2, 10);
|
|
58
|
+
fs.writeFileSync(tmp, e.content, "utf8");
|
|
59
|
+
fs.renameSync(tmp, p);
|
|
60
|
+
} catch {
|
|
61
|
+
/* 忽略恢复失败 */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/evidence/index.js - 证据边界与看板 (吸收 oh-my-hermes / OMH 思路, 纯 JS, 零依赖)
|
|
2
|
+
// 两层证据标记:
|
|
3
|
+
// PREPARED - 注入上下文的"预备材料"(如检索结果/文档), 禁止由 agent 伪造
|
|
4
|
+
// OBSERVED - 工具/环境真实产出的"观测结果", 准入时须校验来源
|
|
5
|
+
// 另含: handoff manifest 哈希清单 (交接不可篡改)、goal board 只读看板、conformance 一致性核查。
|
|
6
|
+
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
|
|
9
|
+
// 证据类型常量
|
|
10
|
+
export const Evidence = Object.freeze({
|
|
11
|
+
PREPARED: "prepared",
|
|
12
|
+
OBSERVED: "observed",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const EVIDENCE_TYPES = new Set([Evidence.PREPARED, Evidence.OBSERVED]);
|
|
16
|
+
|
|
17
|
+
// 浅拷贝对象 (不改动调用方传入的原始引用), 便于 freeze 后安全返回
|
|
18
|
+
function shallowClone(data) {
|
|
19
|
+
if (data === null || typeof data !== "object") return data;
|
|
20
|
+
if (Array.isArray(data)) return [...data];
|
|
21
|
+
if (data instanceof Date) return new Date(data.getTime());
|
|
22
|
+
return { ...data };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 标记为 PREPARED, 附带不可变标记并冻结
|
|
26
|
+
export function markPrepared(data, { source } = {}) {
|
|
27
|
+
if (!source) throw new Error("markPrepared 需要 source 标识来源");
|
|
28
|
+
const obj = shallowClone(data);
|
|
29
|
+
Object.defineProperty(obj, "__evidence", { value: Evidence.PREPARED, enumerable: true, writable: false, configurable: false });
|
|
30
|
+
Object.defineProperty(obj, "__source", { value: String(source), enumerable: true, writable: false, configurable: false });
|
|
31
|
+
Object.defineProperty(obj, "__ts", { value: new Date().toISOString(), enumerable: true, writable: false, configurable: false });
|
|
32
|
+
return Object.freeze(obj);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 标记为 OBSERVED, 附带不可变标记并冻结
|
|
36
|
+
export function markObserved(data, { tool } = {}) {
|
|
37
|
+
if (!tool) throw new Error("markObserved 需要 tool 标识产出工具");
|
|
38
|
+
const obj = shallowClone(data);
|
|
39
|
+
Object.defineProperty(obj, "__evidence", { value: Evidence.OBSERVED, enumerable: true, writable: false, configurable: false });
|
|
40
|
+
Object.defineProperty(obj, "__tool", { value: String(tool), enumerable: true, writable: false, configurable: false });
|
|
41
|
+
Object.defineProperty(obj, "__ts", { value: new Date().toISOString(), enumerable: true, writable: false, configurable: false });
|
|
42
|
+
return Object.freeze(obj);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 校验证据标记存在且类型合法
|
|
46
|
+
export function validateEvidence(obj) {
|
|
47
|
+
if (!obj || typeof obj !== "object") {
|
|
48
|
+
return { valid: false, type: null, reason: "非对象" };
|
|
49
|
+
}
|
|
50
|
+
const t = obj.__evidence;
|
|
51
|
+
if (!EVIDENCE_TYPES.has(t)) {
|
|
52
|
+
return { valid: false, type: null, reason: "缺少合法的 __evidence 标记" };
|
|
53
|
+
}
|
|
54
|
+
if (t === Evidence.PREPARED && !obj.__source) {
|
|
55
|
+
return { valid: false, type: t, reason: "prepared 证据缺少 __source" };
|
|
56
|
+
}
|
|
57
|
+
if (t === Evidence.OBSERVED && !obj.__tool) {
|
|
58
|
+
return { valid: false, type: t, reason: "observed 证据缺少 __tool" };
|
|
59
|
+
}
|
|
60
|
+
if (typeof obj.__ts !== "string") {
|
|
61
|
+
return { valid: false, type: t, reason: "缺少 __ts 时间戳" };
|
|
62
|
+
}
|
|
63
|
+
return { valid: true, type: t, reason: "ok" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// sha256 十六进制 (取前 16 位作为紧凑指纹)
|
|
67
|
+
function shortHash(input) {
|
|
68
|
+
const h = crypto.createHash("sha256").update(typeof input === "string" ? input : JSON.stringify(input)).digest("hex");
|
|
69
|
+
return h.slice(0, 16);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 创建交接清单 (handoff manifest): 每个条目独立哈希 + 整体摘要, 保证交接不可篡改
|
|
73
|
+
export function createHandoffManifest(items = []) {
|
|
74
|
+
const entries = items.map((it) => {
|
|
75
|
+
const data = it && typeof it === "object" && "__evidence" in it ? it : it; // 兼容直接传证据对象或 {data}
|
|
76
|
+
const payload = data && typeof data === "object" && "data" in data ? data.data : data;
|
|
77
|
+
const type = (it && it.type) || (data && data.__evidence) || "unknown";
|
|
78
|
+
const source = (it && it.source) || (data && (data.__source || data.__tool)) || "unknown";
|
|
79
|
+
return {
|
|
80
|
+
hash: shortHash(payload),
|
|
81
|
+
type,
|
|
82
|
+
source,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const digestInput = entries.map((e) => `${e.hash}:${e.type}:${e.source}`).join("|");
|
|
86
|
+
return {
|
|
87
|
+
version: 1,
|
|
88
|
+
created_at: new Date().toISOString(),
|
|
89
|
+
entries,
|
|
90
|
+
digest: shortHash(digestInput),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 目标看板 (只读状态看板, 用于多 agent 协作时统一目标可见性)
|
|
95
|
+
export function createGoalBoard() {
|
|
96
|
+
const goals = new Map(); // id -> {id, title, priority, status}
|
|
97
|
+
const VALID_STATUS = new Set(["pending", "in_progress", "blocked", "done"]);
|
|
98
|
+
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2 };
|
|
99
|
+
|
|
100
|
+
const api = {
|
|
101
|
+
addGoal({ id, title, priority = "P2", status = "pending" } = {}) {
|
|
102
|
+
if (!id) throw new Error("addGoal 需要 id");
|
|
103
|
+
if (!VALID_STATUS.has(status)) status = "pending";
|
|
104
|
+
if (!PRIORITY_RANK.hasOwnProperty(priority)) priority = "P2";
|
|
105
|
+
goals.set(id, { id, title: title || id, priority, status });
|
|
106
|
+
return goals.get(id);
|
|
107
|
+
},
|
|
108
|
+
updateStatus(id, status) {
|
|
109
|
+
const g = goals.get(id);
|
|
110
|
+
if (!g) return null;
|
|
111
|
+
if (!VALID_STATUS.has(status)) throw new Error(`非法状态: ${status}`);
|
|
112
|
+
g.status = status;
|
|
113
|
+
return g;
|
|
114
|
+
},
|
|
115
|
+
list() {
|
|
116
|
+
return [...goals.values()].sort((a, b) => (PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority]));
|
|
117
|
+
},
|
|
118
|
+
get(id) { return goals.get(id) || null; },
|
|
119
|
+
render() {
|
|
120
|
+
const lines = ["# 目标看板 (Goal Board)", ""];
|
|
121
|
+
const order = ["P0", "P1", "P2"];
|
|
122
|
+
const icon = { pending: "○", in_progress: "◑", blocked: "⛔", done: "●" };
|
|
123
|
+
for (const p of order) {
|
|
124
|
+
const items = api.list().filter((g) => g.priority === p);
|
|
125
|
+
if (items.length === 0) continue;
|
|
126
|
+
lines.push(`## ${p}`);
|
|
127
|
+
for (const g of items) {
|
|
128
|
+
const st = icon[g.status] || "?";
|
|
129
|
+
lines.push(`- [${st}] ${g.title} \`${g.id}\` (${g.status})`);
|
|
130
|
+
}
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|
|
133
|
+
return lines.join("\n").trim() + "\n";
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
return api;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// conformance 一致性核查: 用相同算法重算 items 哈希, 与 manifest.entries 比对
|
|
140
|
+
export function conformanceCheck(manifest, items = []) {
|
|
141
|
+
const expected = manifest && manifest.entries ? manifest.entries : [];
|
|
142
|
+
const recomputed = createHandoffManifest(items).entries;
|
|
143
|
+
const mismatches = [];
|
|
144
|
+
const n = Math.max(expected.length, recomputed.length);
|
|
145
|
+
for (let i = 0; i < n; i++) {
|
|
146
|
+
const e = expected[i];
|
|
147
|
+
const r = recomputed[i];
|
|
148
|
+
if (!e || !r || e.hash !== r.hash || e.type !== r.type) {
|
|
149
|
+
mismatches.push({ index: i, expected: e ? e.hash : null, actual: r ? r.hash : null });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { ok: mismatches.length === 0, mismatches, expected, actual: recomputed };
|
|
153
|
+
}
|
package/src/evolve/playbook.js
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
// - 门禁回滚: 任何进化必须过回归基准才 commit, 否则自动 rollback
|
|
7
7
|
// 仅借鉴思想, 源码自研。核心合并/裁剪/门禁纯代码 (可测), LLM 提炼教训走可选 adapter (无 LLM 时跳过)。
|
|
8
8
|
// 与 LearningService.refine (失败→经验库) 互补: refine 沉淀经验, playbook 反哺系统提示词 (零 token 成本时为空)。
|
|
9
|
-
|
|
9
|
+
// ⚠ 接线状态 (2026-09-17 核对): 已由 evolvePlugin 装配为 ctx.provide("playbook"), 但**无内置消费方**
|
|
10
|
+
// —— 没有代码把 playbook bullets 注入 system prompt, 也没有代码在对话后产生 delta 操作。
|
|
11
|
+
// 属"引擎就绪、链路未接"; 待接入 (注入 + delta 生成) 才算生效。
|
|
12
|
+
import { shortId } from "../utils/id.js";
|
|
10
13
|
import path from "node:path";
|
|
11
|
-
import { ensureDir,
|
|
14
|
+
import { ensureDir, readJson, writeJson } from "../utils/store.js";
|
|
12
15
|
|
|
13
16
|
// ---- 词法相似度 (grow-and-refine 用, 零依赖) ----
|
|
14
17
|
// 英文/空格分隔 → 词级 Jaccard; 中文等 CJK 连续串 → 字符 bigram Dice (单 token 时退化)。
|
|
@@ -58,7 +61,7 @@ export function applyDelta(playbook, ops = [], { maxBullets = 200 } = {}) {
|
|
|
58
61
|
// 新增前查重: 与已有 bullet 词法相似 > 0.6 视为重复, 拒收 (grow-and-refine)
|
|
59
62
|
const dup = pb.bullets.find((b) => lexicalSimilarity(b.content, content) > 0.6);
|
|
60
63
|
if (dup) { rejected.push({ op, reason: "重复: " + dup.id }); continue; }
|
|
61
|
-
const id = op.id || ("b"
|
|
64
|
+
const id = op.id || shortId("b", 5);
|
|
62
65
|
const kind = ["strategy", "pitfall", "domain"].includes(op.kind) ? op.kind : "strategy";
|
|
63
66
|
pb.bullets.push({ id, kind, content, counters: { helpful: 0, harmful: 0 }, evidence_ref: op.evidence_ref || null, createdAt: Date.now() });
|
|
64
67
|
applied.push({ op: "ADD", id });
|
|
@@ -141,12 +144,8 @@ export class PlaybookStore {
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
_load() {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const d = JSON.parse(fs.readFileSync(this.file, "utf8"));
|
|
147
|
-
if (d && Array.isArray(d.bullets)) return d;
|
|
148
|
-
}
|
|
149
|
-
} catch {}
|
|
147
|
+
const d = readJson(this.file, null);
|
|
148
|
+
if (d && Array.isArray(d.bullets)) return d;
|
|
150
149
|
return { base: "", bullets: [], version: 1 };
|
|
151
150
|
}
|
|
152
151
|
|
|
@@ -159,7 +158,7 @@ export class PlaybookStore {
|
|
|
159
158
|
|
|
160
159
|
save(pb = this._playbook) {
|
|
161
160
|
this._playbook = pb;
|
|
162
|
-
|
|
161
|
+
writeJson(this.file, pb);
|
|
163
162
|
return pb;
|
|
164
163
|
}
|
|
165
164
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/hooks/index.js — 钩子链 (吸收 claude-code 六事件)
|
|
2
|
+
// 纯 JS、事件驱动注册、超时熔断、单钩子异常不中断链。
|
|
3
|
+
|
|
4
|
+
// claude-code 钩子事件 (PreToolUse 可否决/改参, PostToolUse 可附加上下文)
|
|
5
|
+
export const HOOK_EVENTS = [
|
|
6
|
+
"PreToolUse",
|
|
7
|
+
"PostToolUse",
|
|
8
|
+
"PreCompact",
|
|
9
|
+
"PostCompact",
|
|
10
|
+
"SessionStart",
|
|
11
|
+
"SessionStop",
|
|
12
|
+
"SubagentStop",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// 创建钩子注册表
|
|
16
|
+
export function createHookRegistry() {
|
|
17
|
+
const _hooks = new Map(); // event -> [{ fn, priority, timeoutMs }]
|
|
18
|
+
|
|
19
|
+
// 注册钩子, 返回解绑函数
|
|
20
|
+
function on(event, fn, opts = {}) {
|
|
21
|
+
if (!HOOK_EVENTS.includes(event)) {
|
|
22
|
+
throw new Error("未知钩子事件: " + event);
|
|
23
|
+
}
|
|
24
|
+
if (typeof fn !== "function") {
|
|
25
|
+
throw new Error("钩子 fn 必须是函数");
|
|
26
|
+
}
|
|
27
|
+
const entry = {
|
|
28
|
+
fn,
|
|
29
|
+
priority: opts.priority ?? 100,
|
|
30
|
+
timeoutMs: opts.timeoutMs ?? 3000,
|
|
31
|
+
};
|
|
32
|
+
if (!_hooks.has(event)) _hooks.set(event, []);
|
|
33
|
+
_hooks.get(event).push(entry);
|
|
34
|
+
return function unbind() {
|
|
35
|
+
const arr = _hooks.get(event);
|
|
36
|
+
if (!arr) return;
|
|
37
|
+
const i = arr.indexOf(entry);
|
|
38
|
+
if (i >= 0) arr.splice(i, 1);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 触发事件: 按 priority 升序依次 await 执行
|
|
43
|
+
async function emit(event, payload = {}) {
|
|
44
|
+
const list = (_hooks.get(event) || []).slice().sort((a, b) => a.priority - b.priority);
|
|
45
|
+
const results = [];
|
|
46
|
+
let blocked = false;
|
|
47
|
+
let blockReason = null;
|
|
48
|
+
const additionalContext = [];
|
|
49
|
+
|
|
50
|
+
for (const h of list) {
|
|
51
|
+
let res = null;
|
|
52
|
+
let err = null;
|
|
53
|
+
let timedOut = false;
|
|
54
|
+
try {
|
|
55
|
+
// 2026-09-18 修复: 定时器句柄必须保存并在竞速结束后清理 —— 原实现每次 emit
|
|
56
|
+
// 都留一个活定时器, 钩子正常完成后仍把事件循环挂住 timeoutMs (默认 3s)
|
|
57
|
+
let timer;
|
|
58
|
+
const timeout = new Promise((_, reject) => {
|
|
59
|
+
timer = setTimeout(() => reject(new Error("hook timeout")), h.timeoutMs);
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
res = await Promise.race([Promise.resolve().then(() => h.fn(payload)), timeout]);
|
|
63
|
+
} finally { clearTimeout(timer); }
|
|
64
|
+
} catch (e) {
|
|
65
|
+
err = e && e.message ? e.message : String(e);
|
|
66
|
+
if (/timeout/i.test(err)) timedOut = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (err) {
|
|
70
|
+
results.push({ error: err, timedOut });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
results.push({ result: res });
|
|
74
|
+
|
|
75
|
+
// PreToolUse: 可否决
|
|
76
|
+
if (event === "PreToolUse" && res && res.decision === "block") {
|
|
77
|
+
blocked = true;
|
|
78
|
+
if (!blockReason) blockReason = res.reason || "被钩子否决";
|
|
79
|
+
}
|
|
80
|
+
// PostToolUse: 收集附加上下文
|
|
81
|
+
if (event === "PostToolUse" && res && res.additionalContext != null) {
|
|
82
|
+
additionalContext.push(res.additionalContext);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { blocked, reason: blockReason, results, additionalContext };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { on, emit, events: HOOK_EVENTS.slice() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 生成人类可读的一行日志描述
|
|
93
|
+
export function describeHookEvent(event, payload = {}) {
|
|
94
|
+
const tool = payload.tool || payload.toolName || payload.name || "?";
|
|
95
|
+
switch (event) {
|
|
96
|
+
case "PreToolUse":
|
|
97
|
+
return `PreToolUse: 工具=${tool}`;
|
|
98
|
+
case "PostToolUse":
|
|
99
|
+
return `PostToolUse: 工具=${tool}`;
|
|
100
|
+
case "PreCompact":
|
|
101
|
+
return `PreCompact: 压缩前 (${payload.reason || "manual"})`;
|
|
102
|
+
case "PostCompact":
|
|
103
|
+
return `PostCompact: 压缩后`;
|
|
104
|
+
case "SessionStart":
|
|
105
|
+
return `SessionStart: 会话启动 (${payload.sessionId || "?"})`;
|
|
106
|
+
case "SessionStop":
|
|
107
|
+
return `SessionStop: 会话停止 (${payload.sessionId || "?"})`;
|
|
108
|
+
case "SubagentStop":
|
|
109
|
+
return `SubagentStop: 子代理=${payload.agent || payload.agentId || "?"}`;
|
|
110
|
+
default:
|
|
111
|
+
return `Hook(${event})`;
|
|
112
|
+
}
|
|
113
|
+
}
|