ppxans-harness 2.6.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +195 -195
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +6 -124
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +16 -6
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/agent-professional-training/trajectories/2026-09-11-dpo-pairs.jsonl +2 -0
  16. package/skills/agent-professional-training/trajectories/2026-09-11-round2-trajectory.json +15 -0
  17. package/skills/agent-professional-training/trajectories/2026-09-13-first-run-trajectory.json +17 -0
  18. package/skills/brainstorm/SKILL.md +24 -0
  19. package/skills/cupid-lover-comms/SKILL.md +37 -0
  20. package/skills/debug/SKILL.md +26 -0
  21. package/skills/plan/SKILL.md +25 -0
  22. package/skills/ponytail/SKILL.md +25 -0
  23. package/skills/ppx-memory/SKILL.md +91 -0
  24. package/skills/ppx-memory/scripts/cli.js +192 -0
  25. package/skills/ppx-memory/scripts/experience.js +133 -0
  26. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  27. package/skills/ppx-memory/scripts/l0.js +52 -0
  28. package/skills/ppx-memory/scripts/l2.js +146 -0
  29. package/skills/ppx-memory/scripts/l3.js +112 -0
  30. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  31. package/skills/ppx-memory/scripts/pii.js +42 -0
  32. package/skills/ppx-memory/scripts/schema.js +80 -0
  33. package/skills/ppx-memory/scripts/session.js +398 -0
  34. package/skills/ppx-memory/scripts/similarity.js +43 -0
  35. package/skills/ppx-memory/scripts/store.js +116 -0
  36. package/skills/ppx-memory/scripts/wal.js +38 -0
  37. package/skills/ppx-selfheal/SKILL.md +24 -0
  38. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  39. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  40. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  41. package/skills/ppx-selfheal/scripts/store.js +116 -0
  42. package/skills/prompt-depth-kit/SKILL.md +28 -0
  43. package/skills/session-naming/SKILL.md +36 -0
  44. package/skills/verify/SKILL.md +25 -0
  45. package/src/agent/index.js +1352 -717
  46. package/src/agent/prompts.js +46 -2
  47. package/src/aml-server.js +197 -151
  48. package/src/ans/eviction.js +123 -143
  49. package/src/ans/guard.js +159 -120
  50. package/src/ans/lifecycle.js +96 -93
  51. package/src/ans/proactive.js +112 -129
  52. package/src/ans/reward.js +95 -111
  53. package/src/ans/values.js +15 -15
  54. package/src/audit/audit-chain.js +43 -7
  55. package/src/audit/verifier.js +157 -120
  56. package/src/bus/circuit-breaker.js +9 -1
  57. package/src/bus/runtime-bus.js +107 -93
  58. package/src/channels/base.js +57 -34
  59. package/src/channels/feishu.js +118 -126
  60. package/src/channels/http.js +1090 -651
  61. package/src/channels/index.js +111 -110
  62. package/src/channels/log.js +29 -29
  63. package/src/channels/wechat-crypto.js +73 -74
  64. package/src/channels/wechat.js +191 -197
  65. package/src/channels/workspace.js +94 -0
  66. package/src/channels-cli.js +126 -124
  67. package/src/cli.js +129 -120
  68. package/src/commands/index.js +142 -0
  69. package/src/config/channels.js +137 -170
  70. package/src/config/index.js +277 -224
  71. package/src/config/placeholder.js +31 -0
  72. package/src/config/providers.js +148 -182
  73. package/src/config/settings.js +156 -182
  74. package/src/core/policy.js +69 -18
  75. package/src/core/trace.js +8 -6
  76. package/src/edit/editblock.js +266 -0
  77. package/src/edit/snapshot.js +67 -0
  78. package/src/evidence/index.js +153 -0
  79. package/src/evolve/playbook.js +9 -10
  80. package/src/hooks/index.js +113 -0
  81. package/src/llm/client.js +4 -3
  82. package/src/llm/dsml.js +74 -74
  83. package/src/llm/embedder.js +41 -35
  84. package/src/llm/fence.js +52 -52
  85. package/src/llm/index.js +4 -4
  86. package/src/llm/local-embedder.js +94 -0
  87. package/src/llm/presets.js +113 -0
  88. package/src/llm/pricing.js +93 -0
  89. package/src/llm/retry.js +73 -73
  90. package/src/llm/router.js +92 -89
  91. package/src/mcp/admin.js +44 -10
  92. package/src/mcp/client.js +487 -477
  93. package/src/mcp/http.js +23 -37
  94. package/src/mcp/index.js +116 -116
  95. package/src/mcp/server.js +23 -18
  96. package/src/mcp/tasks.js +9 -14
  97. package/src/memory/asset-hub.js +6 -11
  98. package/src/memory/canvas.js +2 -5
  99. package/src/memory/compaction.js +28 -28
  100. package/src/memory/experience.js +133 -122
  101. package/src/memory/fact-store.js +914 -698
  102. package/src/memory/failure-episode.js +20 -11
  103. package/src/memory/fork.js +17 -8
  104. package/src/memory/index.js +8 -6
  105. package/src/memory/l0.js +53 -52
  106. package/src/memory/l2.js +145 -130
  107. package/src/memory/l3.js +111 -111
  108. package/src/memory/legion-board.js +71 -0
  109. package/src/memory/memory-ticker.js +239 -240
  110. package/src/memory/session.js +398 -397
  111. package/src/memory/sqlite-store.js +581 -0
  112. package/src/mode/blackboard.js +49 -49
  113. package/src/mode/graph.js +42 -41
  114. package/src/mode/index.js +64 -64
  115. package/src/mode/legion.js +54 -51
  116. package/src/mode/plan-exec.js +50 -50
  117. package/src/mode/router.js +28 -40
  118. package/src/orchestrator/agent-worker.js +69 -69
  119. package/src/orchestrator/dag.js +90 -83
  120. package/src/orchestrator/experts.js +76 -0
  121. package/src/orchestrator/index.js +1 -1
  122. package/src/orchestrator/legion.js +179 -187
  123. package/src/orchestrator/supervisor.js +6 -8
  124. package/src/permissions/index.js +378 -0
  125. package/src/persona/index.js +28 -29
  126. package/src/plugin/builtin.js +313 -215
  127. package/src/plugin/context.js +80 -79
  128. package/src/plugin/index.js +62 -62
  129. package/src/plugin/v3.js +73 -0
  130. package/src/protocol/index.js +148 -0
  131. package/src/repomap/index.js +309 -0
  132. package/src/review/index.js +393 -0
  133. package/src/seam/registry.js +3 -0
  134. package/src/seam/shell.js +55 -55
  135. package/src/security/injection.js +79 -0
  136. package/src/selfheal/evolve.js +67 -67
  137. package/src/selfheal/healer.js +184 -167
  138. package/src/selfheal/run.js +9 -9
  139. package/src/server.js +63 -60
  140. package/src/services/diagnose.js +180 -0
  141. package/src/services/learning-service.js +9 -0
  142. package/src/services/memory-health.js +34 -6
  143. package/src/services/memory-service.js +42 -9
  144. package/src/services/triage.js +138 -0
  145. package/src/session/parts.js +76 -0
  146. package/src/session/projection.js +73 -0
  147. package/src/session/rollout.js +54 -0
  148. package/src/session/turn.js +137 -0
  149. package/src/skills/lint.js +72 -0
  150. package/src/skills/loader.js +231 -150
  151. package/src/skills/search.js +58 -0
  152. package/src/skills/verify.js +95 -100
  153. package/src/tools/advanced.js +388 -352
  154. package/src/tools/builtin.js +384 -297
  155. package/src/tools/catalog.js +283 -159
  156. package/src/tools/command-guard.js +112 -112
  157. package/src/tools/custom.js +47 -47
  158. package/src/tools/delegate.js +383 -297
  159. package/src/tools/document.js +254 -253
  160. package/src/tools/git.js +151 -0
  161. package/src/tools/governance.js +47 -20
  162. package/src/tools/index.js +16 -11
  163. package/src/tools/methods.js +178 -178
  164. package/src/tools/ocr.js +59 -59
  165. package/src/tools/sandbox-worker.js +40 -0
  166. package/src/tools/sandbox.js +92 -0
  167. package/src/tools/seam.js +162 -125
  168. package/src/tools/selfmod.js +196 -176
  169. package/src/tools/v3.js +225 -0
  170. package/src/tools/vad.js +176 -0
  171. package/src/tools/voice.js +238 -0
  172. package/src/utils/async.js +14 -0
  173. package/src/utils/config-file.js +53 -0
  174. package/src/utils/crashguard.js +88 -0
  175. package/src/utils/http.js +53 -0
  176. package/src/utils/id.js +8 -0
  177. package/src/utils/json-state.js +33 -0
  178. package/src/utils/logger.js +17 -17
  179. package/src/utils/ndjson.js +25 -0
  180. package/src/utils/pii.js +42 -42
  181. package/src/utils/rate-limit.js +50 -0
  182. package/src/utils/schema.js +80 -0
  183. package/src/utils/similarity.js +43 -0
  184. package/src/utils/store.js +170 -108
  185. package/src/utils/text.js +15 -15
  186. package/src/utils/trace.js +153 -153
  187. package/src/utils/wal.js +39 -0
  188. package/src/utils/winutf8.js +16 -15
  189. package/src/wiki/index.js +170 -0
@@ -0,0 +1,54 @@
1
+ // src/session/rollout.js - JSONL rollout 持久化 (对齐 codex rollout + fork/rewind)
2
+ // item 约定: { seq, type, payload }
3
+ // - append: 追加一行 JSON, 目录不存在先建
4
+ // - fork: 从源会话复制 seq<=uptoSeq 的 items 到新会话文件
5
+ // - rewind: 删除 seq>uptoSeq 的行, 先写 .tmp 再 rename (Windows 安全: 避免读半截)
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ export const Rollout = {
10
+ // 会话 rollout 文件路径 (sessionId 做安全化, 防路径穿越)
11
+ _file(dir, sessionId) {
12
+ const safe = String(sessionId).replace(/[^\w.-]/g, "_");
13
+ return path.join(dir, `${safe}.rollout.jsonl`);
14
+ },
15
+
16
+ // 追加一行 JSON ({ts, ...item})
17
+ append(dir, sessionId, item) {
18
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
19
+ const line = JSON.stringify({ ts: Date.now(), ...item }) + "\n";
20
+ fs.appendFileSync(this._file(dir, sessionId), line, "utf8");
21
+ },
22
+
23
+ // 读取全部 items (按文件顺序)
24
+ load(dir, sessionId) {
25
+ const file = this._file(dir, sessionId);
26
+ const out = [];
27
+ try {
28
+ for (const l of fs.readFileSync(file, "utf8").split("\n").filter(Boolean)) {
29
+ try { out.push(JSON.parse(l)); } catch { /* 跳过损坏行 */ }
30
+ }
31
+ } catch { /* 文件不存在视为空 */ }
32
+ return out;
33
+ },
34
+
35
+ // 从 fromSessionId 复制 seq<=uptoSeq 的 items 到 newSessionId 文件 (覆盖写, 目标为新会话)
36
+ fork(dir, fromSessionId, newSessionId, uptoSeq) {
37
+ const items = this.load(dir, fromSessionId).filter((it) => it.seq <= uptoSeq);
38
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
39
+ const content = items.map((it) => JSON.stringify(it)).join("\n") + (items.length ? "\n" : "");
40
+ fs.writeFileSync(this._file(dir, newSessionId), content, "utf8");
41
+ return items;
42
+ },
43
+
44
+ // 回退: 删除 seq>uptoSeq 的行 (先写 .tmp 再 rename)
45
+ rewind(dir, sessionId, uptoSeq) {
46
+ const items = this.load(dir, sessionId).filter((it) => it.seq <= uptoSeq);
47
+ const file = this._file(dir, sessionId);
48
+ const tmp = file + ".tmp";
49
+ const content = items.map((it) => JSON.stringify(it)).join("\n") + (items.length ? "\n" : "");
50
+ fs.writeFileSync(tmp, content, "utf8");
51
+ fs.renameSync(tmp, file);
52
+ return items;
53
+ },
54
+ };
@@ -0,0 +1,137 @@
1
+ // src/session/turn.js - Session→Task→Turn 状态机 (对齐 codex taskTurn/turn 模型)
2
+ // Session 持有有序 tasks; Task 持有有序 turns + queuedUserInputs;
3
+ // turn 完成后若仍有排队输入则自动续开新 turn (对齐 codex appendTurn/completeTurn 语义)。
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ // 新建一个 turn 对象 (status: open/closed/aborted)
7
+ function createTurn(task, seq) {
8
+ return {
9
+ id: `turn_${randomUUID()}`,
10
+ seq,
11
+ status: "open",
12
+ startedAt: Date.now(),
13
+ closedAt: null,
14
+ items: [], // ResponseItem 数组
15
+ };
16
+ }
17
+
18
+ // 新建一个 task 对象, 方法通过 this 绑定 (便于反序列化复用同一份方法)
19
+ function newTask(session, id) {
20
+ return {
21
+ id: id || `task_${randomUUID()}`,
22
+ sessionId: session.id,
23
+ turns: [],
24
+ status: "running", // running | complete | aborted
25
+ queuedUserInputs: [],
26
+ currentTurn: null,
27
+
28
+ // 追加一个用户输入:
29
+ // - 当前 turn 进行中 -> 先排队 (用户追打)
30
+ // - 无进行中 turn -> 开新 turn 承接
31
+ appendTurn(userInput) {
32
+ if (this.currentTurn && this.currentTurn.status === "open") {
33
+ this.queuedUserInputs.push(userInput);
34
+ return this.currentTurn;
35
+ }
36
+ const turn = createTurn(this, this.turns.length + 1);
37
+ this.turns.push(turn);
38
+ this.currentTurn = turn;
39
+ turn.items.push(userInput);
40
+ session.turnCount += 1;
41
+ return turn;
42
+ },
43
+
44
+ // 结束当前 turn:
45
+ // - 当前 turn 标 closed
46
+ // - 若仍有排队输入, 自动开新 turn 承接第一个 (其余留待下次 completeTurn)
47
+ completeTurn() {
48
+ if (this.currentTurn) {
49
+ this.currentTurn.status = "closed";
50
+ this.currentTurn.closedAt = Date.now();
51
+ }
52
+ if (this.queuedUserInputs.length > 0) {
53
+ const next = this.queuedUserInputs.shift();
54
+ this.appendTurn(next);
55
+ }
56
+ return this.currentTurn;
57
+ },
58
+
59
+ // 中止: 当前 turn 标 aborted, task 标 aborted
60
+ abort() {
61
+ if (this.currentTurn) {
62
+ this.currentTurn.status = "aborted";
63
+ this.currentTurn.closedAt = Date.now();
64
+ }
65
+ this.status = "aborted";
66
+ session.status = "aborted";
67
+ return this;
68
+ },
69
+ };
70
+ }
71
+
72
+ // 创建会话
73
+ export function createSession({ id } = {}) {
74
+ const sessionId = id || `sess_${randomUUID()}`;
75
+ const session = {
76
+ id: sessionId,
77
+ tasks: [],
78
+ turnCount: 0,
79
+ status: "active", // active | aborted
80
+ createTask(input = {}) {
81
+ const task = newTask(this, input.id);
82
+ this.tasks.push(task);
83
+ return task;
84
+ },
85
+ currentTask() {
86
+ return this.tasks.length ? this.tasks[this.tasks.length - 1] : null;
87
+ },
88
+ };
89
+ return session;
90
+ }
91
+
92
+ // 序列化会话为 JSON 字符串 (仅数据, 方法在反序列化时重建)
93
+ export function serializeSession(session) {
94
+ return JSON.stringify({
95
+ id: session.id,
96
+ turnCount: session.turnCount,
97
+ status: session.status,
98
+ tasks: session.tasks.map((t) => ({
99
+ id: t.id,
100
+ sessionId: t.sessionId,
101
+ status: t.status,
102
+ queuedUserInputs: t.queuedUserInputs,
103
+ turns: t.turns.map((tn) => ({
104
+ id: tn.id,
105
+ seq: tn.seq,
106
+ status: tn.status,
107
+ startedAt: tn.startedAt,
108
+ closedAt: tn.closedAt,
109
+ items: tn.items,
110
+ })),
111
+ })),
112
+ });
113
+ }
114
+
115
+ // 从 JSON 反序列化 (重建方法 + 当前 turn 指针)
116
+ export function deserializeSession(json) {
117
+ const data = typeof json === "string" ? JSON.parse(json) : json;
118
+ const session = createSession({ id: data.id });
119
+ session.turnCount = data.turnCount ?? 0;
120
+ session.status = data.status ?? "active";
121
+ session.tasks = (data.tasks || []).map((t) => {
122
+ const task = newTask(session, t.id);
123
+ task.status = t.status ?? "running";
124
+ task.queuedUserInputs = t.queuedUserInputs || [];
125
+ task.turns = (t.turns || []).map((tn) => ({
126
+ id: tn.id,
127
+ seq: tn.seq,
128
+ status: tn.status,
129
+ startedAt: tn.startedAt,
130
+ closedAt: tn.closedAt ?? null,
131
+ items: tn.items || [],
132
+ }));
133
+ task.currentTurn = task.turns.length ? task.turns[task.turns.length - 1] : null;
134
+ return task;
135
+ });
136
+ return session;
137
+ }
@@ -0,0 +1,72 @@
1
+ // src/skills/lint.js - Skill 质量门槛 (零 LLM, 可接 CI)
2
+ // 依据《Skill 蓝皮书 2026》: 元数据与描述质量是 skill 分发的第一瓶颈。
3
+ // error = 阻断 (结构/元数据缺失, 不可分发)
4
+ // warning = 建议 (描述过泛/缺反合理化段, 影响模型能否正确选中)
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { parseFrontmatter, stripFrontmatter, parseSections } from "./loader.js";
8
+
9
+ export const MIN_DESC_LEN = 8;
10
+ export const MIN_BODY_LEN = 40;
11
+ const NAME_RE = /^[a-zA-Z0-9-]+$/;
12
+
13
+ function lintOne(dir, id) {
14
+ const errors = [];
15
+ const warnings = [];
16
+ const file = path.join(dir, id, "SKILL.md");
17
+ if (!fs.existsSync(file)) {
18
+ errors.push({ id: "no-skill-md", msg: "缺少 SKILL.md" });
19
+ return { name: id, ok: false, errors, warnings };
20
+ }
21
+ let raw = "";
22
+ try {
23
+ raw = fs.readFileSync(file, "utf8");
24
+ } catch (e) {
25
+ errors.push({ id: "unreadable", msg: "SKILL.md 读取失败: " + String(e?.message || e) });
26
+ return { name: id, ok: false, errors, warnings };
27
+ }
28
+ const fm = parseFrontmatter(raw);
29
+ const body = stripFrontmatter(raw).trim();
30
+
31
+ if (!fm.name) errors.push({ id: "missing-name", msg: "frontmatter 缺 name" });
32
+ else if (fm.name !== id) errors.push({ id: "name-mismatch", msg: `name (${fm.name}) 与目录名 (${id}) 不一致` });
33
+ if (fm.name && !NAME_RE.test(fm.name)) errors.push({ id: "bad-name", msg: "name 仅允许字母/数字/横线" });
34
+
35
+ if (!fm.description) errors.push({ id: "missing-description", msg: "frontmatter 缺 description (模型选中的唯一依据)" });
36
+ else if (String(fm.description).trim().length < MIN_DESC_LEN) {
37
+ warnings.push({ id: "short-description", msg: `description 过短 (< ${MIN_DESC_LEN} 字), 模型难以判断适用场景` });
38
+ }
39
+
40
+ if (!body) errors.push({ id: "empty-body", msg: "正文为空" });
41
+ else if (body.length < MIN_BODY_LEN) {
42
+ warnings.push({ id: "short-body", msg: `正文过短 (< ${MIN_BODY_LEN} 字), 建议补流程/检查点` });
43
+ }
44
+
45
+ const secs = parseSections(body);
46
+ if (!secs["流程"]) errors.push({ id: "no-process", msg: '缺 "## 流程" 段' });
47
+ if (!secs["验证"]) errors.push({ id: "no-verify", msg: '缺 "## 验证" 段 (完成后必须提供的证据)' });
48
+ if (!secs["反合理化"]) warnings.push({ id: "no-antirationale", msg: '缺 "## 反合理化" 段 (常见偷懒借口 + 反驳)' });
49
+
50
+ return { name: id, ok: errors.length === 0, errors, warnings };
51
+ }
52
+
53
+ export function lintSkillDir(dir) {
54
+ if (!fs.existsSync(dir)) return { results: [], pass: 0, warn: 0, fail: 0, error: `技能目录不存在: ${dir}` };
55
+ let ids = [];
56
+ try {
57
+ ids = fs
58
+ .readdirSync(dir, { withFileTypes: true })
59
+ .filter((e) => e.isDirectory())
60
+ .map((e) => e.name)
61
+ .filter((n) => !n.startsWith("."))
62
+ .sort();
63
+ } catch (e) {
64
+ return { results: [], pass: 0, warn: 0, fail: 0, error: `技能目录不可读: ${String(e?.message || e)}` };
65
+ }
66
+
67
+ const results = ids.map((id) => lintOne(dir, id));
68
+ const pass = results.filter((r) => r.ok && !r.warnings.length).length;
69
+ const warn = results.filter((r) => r.ok && r.warnings.length).length;
70
+ const fail = results.filter((r) => !r.ok).length;
71
+ return { results, pass, warn, fail, error: null };
72
+ }
@@ -1,150 +1,231 @@
1
- // src/skills/loader.js - Skill Catalog + Loader
2
- // 参考 deepseek-harness 的 skill package(catalog + loader): 可枚举、可发现、可加载的技能注册表
3
- // 扫描 <skillsDir>/*/SKILL.md, 解析 frontmatter(name/description), 提供 list/get/loadAll
4
- import fs from "node:fs";
5
- import path from "node:path";
6
-
7
- // 解析 SKILL.md 顶部的 --- frontmatter ---
8
- // 支持简单 `key: value` 和 YAML 折叠块(`>` / `|`),保证多行 description 可被解析。
9
- export function parseFrontmatter(md) {
10
- const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
11
- if (!m) return {};
12
- const meta = {};
13
- const lines = m[1].split(/\r?\n/);
14
- let key = null;
15
- let block = null;
16
- let blockLines = [];
17
- const flush = () => {
18
- if (key && block) {
19
- meta[key] = block === ">"
20
- ? blockLines.join(" ").replace(/\s+/g, " ").trim()
21
- : blockLines.join("\n").trim();
22
- }
23
- key = null;
24
- block = null;
25
- blockLines = [];
26
- };
27
- for (const line of lines) {
28
- const mm = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
29
- if (mm) {
30
- flush();
31
- key = mm[1];
32
- const val = mm[2].trim();
33
- if (val === ">" || val === "|") {
34
- block = val;
35
- } else {
36
- meta[key] = val;
37
- key = null;
38
- }
39
- } else if (block && key) {
40
- blockLines.push(line.trim());
41
- }
42
- }
43
- flush();
44
- return meta;
45
- }
46
-
47
- // 解析 SKILL.md 正文章节: "## 标题" -> 内容
48
- // 供按需加载 (渐进式披露): agent 可只读「反合理化」「验证」等特定段, 不用整篇读入
49
- export function parseSections(md) {
50
- const sections = {};
51
- const lines = String(md || "").split(/\r?\n/);
52
- let cur = null;
53
- for (const line of lines) {
54
- const m = line.match(/^##\s+(.+)$/);
55
- if (m) { cur = m[1].trim(); sections[cur] = ""; continue; }
56
- if (cur) sections[cur] += line + "\n";
57
- }
58
- for (const k of Object.keys(sections)) sections[k] = sections[k].trim();
59
- return sections;
60
- }
61
-
62
- export class SkillLoader {
63
- constructor(skillsDir) {
64
- this.dir = skillsDir;
65
- // 技能使用追踪 (source: Hermes "skill self-improves during use")
66
- this.usageFile = path.join(skillsDir, ".usage.json");
67
- this._usage = this._readUsage();
68
- }
69
-
70
- _scan() {
71
- if (!fs.existsSync(this.dir)) return {};
72
- const out = {};
73
- for (const entry of fs.readdirSync(this.dir, { withFileTypes: true })) {
74
- if (!entry.isDirectory()) continue;
75
- const skillMd = path.join(this.dir, entry.name, "SKILL.md");
76
- if (!fs.existsSync(skillMd)) continue;
77
- const raw = fs.readFileSync(skillMd, "utf8");
78
- const meta = parseFrontmatter(raw);
79
- out[entry.name] = {
80
- id: entry.name,
81
- name: meta.name || entry.name,
82
- description: meta.description || "",
83
- dir: path.join(this.dir, entry.name),
84
- path: skillMd,
85
- };
86
- }
87
- return out;
88
- }
89
-
90
- // 可枚举: 全部技能
91
- list() {
92
- return Object.values(this._scan());
93
- }
94
-
95
- // 可发现: 按 id 查
96
- get(id) {
97
- return this._scan()[id] || null;
98
- }
99
-
100
- has(id) {
101
- return !!this._scan()[id];
102
- }
103
-
104
- // 读取某个技能的完整 SKILL.md 内容
105
- read(id) {
106
- const s = this.get(id);
107
- if (!s) return null;
108
- return fs.readFileSync(s.path, "utf8");
109
- }
110
-
111
- // 按需读取某个技能的指定章节 (渐进式披露: 只读「反合理化」「验证」等段, 省 token)
112
- readSection(id, section) {
113
- const raw = this.read(id);
114
- if (raw === null) return null;
115
- const sections = parseSections(raw);
116
- return sections[section] ?? null;
117
- }
118
-
119
- _readUsage() {
120
- try { if (fs.existsSync(this.usageFile)) { const d = JSON.parse(fs.readFileSync(this.usageFile, "utf8")); if (d && typeof d === "object") return d; } } catch {}
121
- return {};
122
- }
123
-
124
- _saveUsage() {
125
- try { fs.writeFileSync(this.usageFile, JSON.stringify(this._usage, null, 2), "utf8"); } catch {}
126
- }
127
-
128
- // 记录一次技能使用 (load_skill 时调用)
129
- trackUse(id) {
130
- this._usage = this._readUsage();
131
- const u = this._usage[id] || { uses: 0, lastUsed: null };
132
- u.uses = (u.uses || 0) + 1;
133
- u.lastUsed = new Date().toISOString();
134
- this._usage[id] = u;
135
- this._saveUsage();
136
- return u;
137
- }
138
-
139
- // 某技能使用统计
140
- useOf(id) { const u = this._readUsage()[id]; return u ? { uses: u.uses || 0, lastUsed: u.lastUsed || null } : { uses: 0, lastUsed: null }; }
141
-
142
- // 全部技能使用统计
143
- usageAll() { return this._readUsage(); }
144
-
145
- // 重置某技能使用计数 (用中自进化升级完成后调用, 防连跑)
146
- resetUse(id) {
147
- this._usage = this._readUsage();
148
- if (this._usage[id]) { this._usage[id].uses = 0; this._usage[id].lastUpgraded = new Date().toISOString(); this._saveUsage(); }
149
- }
150
- }
1
+ // src/skills/loader.js - 方法技能加载器 (SKILL.md 目录式)
2
+ // 来源: Superpowers / addyosmani-agent-skills 模式 —— 技能=目录 + SKILL.md(frontmatter + ## 章节)
3
+ // 设计要点:
4
+ // 1) 签名缓存 (mtime+size): 内容修改/目录增删后 list()/get() 必须反映变化 (蓝皮书: 发现是生态瓶颈)
5
+ // 2) readSection 按需读章节: 只把命中的章节喂给 LLM, 省 token
6
+ // 3) 使用追踪 trackUse/useOf/usageAll: 用中自进化 (Hermes) 的前提是知道谁在用、谁闲置
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+
10
+ // ---- frontmatter / 章节解析 (零依赖) ----
11
+
12
+ // 解析 `---\nkey: value\n---` 头。无头返回 {}。
13
+ export function parseFrontmatter(md) {
14
+ const text = String(md || "").replace(/^\uFEFF/, "");
15
+ const m = text.match(/^\s*---\s*\r?\n([\s\S]*?)\r?\n\s*---\s*(?:\r?\n|$)/);
16
+ if (!m) return {};
17
+ const out = {};
18
+ for (const line of m[1].split(/\r?\n/)) {
19
+ const i = line.indexOf(":");
20
+ if (i <= 0) continue;
21
+ const k = line.slice(0, i).trim();
22
+ let v = line.slice(i + 1).trim();
23
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
24
+ v = v.slice(1, -1);
25
+ }
26
+ if (k) out[k] = v;
27
+ }
28
+ return out;
29
+ }
30
+
31
+ // 去掉 frontmatter 后的正文
32
+ export function stripFrontmatter(md) {
33
+ const text = String(md || "").replace(/^\uFEFF/, "");
34
+ const m = text.match(/^\s*---\s*\r?\n[\s\S]*?\r?\n\s*---\s*(?:\r?\n|$)/);
35
+ return m ? text.slice(m[0].length) : text;
36
+ }
37
+
38
+ // 解析二级标题章节: `## 流程` → { "流程": "正文(不含标题行)" }
39
+ export function parseSections(md) {
40
+ const body = stripFrontmatter(md);
41
+ const out = {};
42
+ const lines = body.split(/\r?\n/);
43
+ let cur = null;
44
+ let buf = [];
45
+ const flush = () => {
46
+ if (cur === null) return;
47
+ out[cur] = buf.join("\n").trim();
48
+ };
49
+ for (const line of lines) {
50
+ const h = line.match(/^##\s+(.+?)\s*$/);
51
+ const h1 = line.match(/^#\s+(.+?)\s*$/);
52
+ if (h) {
53
+ flush();
54
+ cur = h[1].trim();
55
+ buf = [];
56
+ } else if (h1) {
57
+ // 一级标题不作为章节, 但结束当前章节
58
+ flush();
59
+ cur = null;
60
+ buf = [];
61
+ } else if (cur !== null) {
62
+ buf.push(line);
63
+ }
64
+ }
65
+ flush();
66
+ return out;
67
+ }
68
+
69
+ const USAGE_FILE = ".usage.json";
70
+
71
+ function readJsonSafe(file, fallback) {
72
+ try {
73
+ if (!fs.existsSync(file)) return fallback;
74
+ const raw = fs.readFileSync(file, "utf8");
75
+ if (!raw.trim()) return fallback;
76
+ const v = JSON.parse(raw);
77
+ return v && typeof v === "object" ? v : fallback;
78
+ } catch {
79
+ return fallback;
80
+ }
81
+ }
82
+
83
+ export class SkillLoader {
84
+ constructor(dir) {
85
+ this.dir = dir;
86
+ this._cache = new Map(); // id -> { sig, meta }
87
+ }
88
+
89
+ // 单技能签名: SKILL.md 的 mtime+size (变更即失效缓存)
90
+ _sigOf(file) {
91
+ try {
92
+ const st = fs.statSync(file);
93
+ return `${st.mtimeMs}:${st.size}`;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ _skillDirs() {
100
+ try {
101
+ return fs
102
+ .readdirSync(this.dir, { withFileTypes: true })
103
+ .filter((e) => e.isDirectory())
104
+ .map((e) => e.name)
105
+ .filter((n) => !n.startsWith(".") && n !== "node_modules");
106
+ } catch {
107
+ return [];
108
+ }
109
+ }
110
+
111
+ _load(id) {
112
+ const file = path.join(this.dir, id, "SKILL.md");
113
+ const sig = this._sigOf(file);
114
+ if (sig === null) {
115
+ this._cache.delete(id);
116
+ return null;
117
+ }
118
+ const hit = this._cache.get(id);
119
+ if (hit && hit.sig === sig) return hit.meta;
120
+ let raw = "";
121
+ try {
122
+ raw = fs.readFileSync(file, "utf8");
123
+ } catch {
124
+ this._cache.delete(id);
125
+ return null;
126
+ }
127
+ const fm = parseFrontmatter(raw);
128
+ const meta = {
129
+ id,
130
+ name: String(fm.name || id),
131
+ description: String(fm.description || ""),
132
+ frontmatter: fm,
133
+ body: stripFrontmatter(raw),
134
+ file,
135
+ };
136
+ this._cache.set(id, { sig, meta });
137
+ return meta;
138
+ }
139
+
140
+ // 列出全部技能 (id/name/description), 目录增删与内容修改都会反映
141
+ list() {
142
+ const ids = this._skillDirs().sort();
143
+ const keep = new Set(ids);
144
+ for (const k of [...this._cache.keys()]) if (!keep.has(k)) this._cache.delete(k);
145
+ const out = [];
146
+ for (const id of ids) {
147
+ const m = this._load(id);
148
+ if (m) out.push({ id: m.id, name: m.name, description: m.description });
149
+ }
150
+ return out;
151
+ }
152
+
153
+ has(id) {
154
+ return this._load(String(id || "")) !== null;
155
+ }
156
+
157
+ get(id) {
158
+ const m = this._load(String(id || ""));
159
+ if (!m) return null;
160
+ return { id: m.id, name: m.name, description: m.description, frontmatter: m.frontmatter };
161
+ }
162
+
163
+ // SKILL.md 正文 (去 frontmatter)。未知技能返回 null。
164
+ read(id) {
165
+ const m = this._load(String(id || ""));
166
+ return m ? m.body : null;
167
+ }
168
+
169
+ // 按需读章节。章节不存在或技能不存在返回 null。
170
+ readSection(id, section) {
171
+ const body = this.read(id);
172
+ if (body === null) return null;
173
+ const s = parseSections(body);
174
+ const v = s[String(section || "")];
175
+ return v === undefined ? null : v;
176
+ }
177
+
178
+ // ---- 使用追踪 (落盘, 跨实例可读回) ----
179
+ _usageFile() {
180
+ return path.join(this.dir, USAGE_FILE);
181
+ }
182
+
183
+ _readUsage() {
184
+ return readJsonSafe(this._usageFile(), {});
185
+ }
186
+
187
+ _writeUsage(u) {
188
+ try {
189
+ fs.mkdirSync(this.dir, { recursive: true });
190
+ fs.writeFileSync(this._usageFile(), JSON.stringify(u, null, 2), "utf8");
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ trackUse(id) {
198
+ const key = String(id || "");
199
+ if (!key) return { uses: 0, lastUsed: null };
200
+ const u = this._readUsage();
201
+ const cur = u[key] && typeof u[key] === "object" ? u[key] : { uses: 0, lastUsed: null };
202
+ const next = { uses: Number(cur.uses || 0) + 1, lastUsed: new Date().toISOString() };
203
+ u[key] = next;
204
+ this._writeUsage(u);
205
+ return next;
206
+ }
207
+
208
+ useOf(id) {
209
+ const u = this._readUsage();
210
+ const cur = u[String(id || "")];
211
+ if (!cur || typeof cur !== "object") return { uses: 0, lastUsed: null };
212
+ return { uses: Number(cur.uses || 0), lastUsed: cur.lastUsed || null };
213
+ }
214
+
215
+ usageAll() {
216
+ const u = this._readUsage();
217
+ const out = {};
218
+ for (const [k, v] of Object.entries(u)) {
219
+ if (!v || typeof v !== "object") continue;
220
+ out[k] = { uses: Number(v.uses || 0), lastUsed: v.lastUsed || null };
221
+ }
222
+ return out;
223
+ }
224
+
225
+ resetUse(id) {
226
+ const key = String(id || "");
227
+ const u = this._readUsage();
228
+ delete u[key];
229
+ return this._writeUsage(u);
230
+ }
231
+ }