ppxans-harness 2.6.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 -262
- 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 +6 -124
- package/config/ppx.json.example +143 -0
- package/package.json +16 -6
- 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 +46 -2
- 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 -651
- 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 -182
- 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 +4 -3
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -52
- 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 -89
- package/src/mcp/admin.js +44 -10
- package/src/mcp/client.js +487 -477
- package/src/mcp/http.js +23 -37
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +23 -18
- package/src/mcp/tasks.js +9 -14
- 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 -215
- 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,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
|
+
}
|
package/src/skills/loader.js
CHANGED
|
@@ -1,150 +1,231 @@
|
|
|
1
|
-
// src/skills/loader.js -
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
}
|