@shgroup/dsh-serenity-hooks 1.17.4 → 1.18.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/dsh.plugin.json +1 -1
- package/lib/index.js +339 -31
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/loop + 拦截缝机械约束(safe-mode/路径守卫/会话落盘)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。私有(dsh-external 组织)。",
|
|
6
6
|
"engines": {
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
2
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
-
import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
@@ -71,12 +71,19 @@ function resolveInside(root, p) {
|
|
|
71
71
|
* .dsh 仅作 dsh 运行时回退,不优先)。
|
|
72
72
|
*/
|
|
73
73
|
const DEFAULT_SERENITY_CONFIG_PATHS = [".opencode/serenity.json", ".dsh/serenity.json"];
|
|
74
|
+
/**
|
|
75
|
+
* 读取 UTF-8 文件并剥离 BOM(Windows 审计问题 16):PowerShell/Windows 编辑器
|
|
76
|
+
* 写出的 BOM(\uFEFF)会让 JSON.parse 抛错(配置静默变空)或 frontmatter 检测失败(技能被丢弃)。
|
|
77
|
+
*/
|
|
78
|
+
function readUtf8(path) {
|
|
79
|
+
return readFileSync(path, "utf-8").replace(/^\uFEFF/, "");
|
|
80
|
+
}
|
|
74
81
|
function loadSerenityConfig(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
75
82
|
for (const candidate of paths) {
|
|
76
83
|
const p = resolve(root, candidate);
|
|
77
84
|
if (!existsSync(p)) continue;
|
|
78
85
|
try {
|
|
79
|
-
return JSON.parse(
|
|
86
|
+
return JSON.parse(readUtf8(p));
|
|
80
87
|
} catch {
|
|
81
88
|
return {};
|
|
82
89
|
}
|
|
@@ -183,13 +190,22 @@ function safeRel(root, abs) {
|
|
|
183
190
|
}
|
|
184
191
|
function validateWritePath(root, target) {
|
|
185
192
|
const absPath = target.startsWith("/") ? resolve(target) : resolveInside(root, target);
|
|
186
|
-
if (
|
|
187
|
-
|
|
193
|
+
if (!pathInside(resolve(root), absPath)) throw new Error(`cc-fs: path "${target}" resolves to "${absPath}" which is outside serenity root "${root}"`);
|
|
194
|
+
const rel = relative(root, absPath).split("\\").join("/");
|
|
195
|
+
if (rel.endsWith("/mech-registry.json") && rel.includes("/.opencode/skills/")) throw new Error(`cc-fs: refusing to directly modify mech-registry.json — use acc_msm register/deregister instead`);
|
|
196
|
+
if (existsSync(absPath)) try {
|
|
197
|
+
const real = realpathSync(absPath);
|
|
198
|
+
if (!pathInside(resolve(root), real)) throw new Error(`cc-fs: path "${target}" resolves via symlink to "${real}" outside serenity root "${root}"`);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (e instanceof Error && e.message.includes("symlink")) throw e;
|
|
201
|
+
}
|
|
188
202
|
return absPath;
|
|
189
203
|
}
|
|
190
204
|
function assertNotProtected(root, absPath, targetLabel) {
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
const ci = process.platform === "win32";
|
|
206
|
+
const eq = (a, b) => ci ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
207
|
+
if (eq(absPath, resolve(root, ".serenity"))) throw new Error(`cc-fs: refusing to delete protected path: ${targetLabel} (.serenity is the CCC marker)`);
|
|
208
|
+
if (eq(absPath, root)) throw new Error(`cc-fs: refusing to delete the CCC root directory: ${targetLabel}`);
|
|
193
209
|
}
|
|
194
210
|
function runCcFs(root, args) {
|
|
195
211
|
const a = args.action;
|
|
@@ -306,7 +322,12 @@ function runCcFs(root, args) {
|
|
|
306
322
|
recursive: true,
|
|
307
323
|
force: false
|
|
308
324
|
});
|
|
309
|
-
else
|
|
325
|
+
else {
|
|
326
|
+
if (process.platform === "win32") try {
|
|
327
|
+
chmodSync(absPath, 438);
|
|
328
|
+
} catch {}
|
|
329
|
+
unlinkSync(absPath);
|
|
330
|
+
}
|
|
310
331
|
results.push(`[OK] deleted: ${relLabel}`);
|
|
311
332
|
}
|
|
312
333
|
return results.join("\n");
|
|
@@ -367,7 +388,7 @@ function runCcFs(root, args) {
|
|
|
367
388
|
const revealPath = statSync(absPath).isDirectory() ? absPath : dirname(absPath);
|
|
368
389
|
execFileSync("xdg-open", [revealPath], { timeout: 1e4 });
|
|
369
390
|
} else if (os === "win32") {
|
|
370
|
-
const winArgs = statSync(absPath).isDirectory() ? [absPath] : [
|
|
391
|
+
const winArgs = statSync(absPath).isDirectory() ? [absPath] : [`/select,${absPath}`];
|
|
371
392
|
const child = spawn("explorer", winArgs, {
|
|
372
393
|
detached: true,
|
|
373
394
|
stdio: "ignore",
|
|
@@ -712,7 +733,7 @@ safe-mode 由 WebUI 开关控制(写 .serenity-safe-on 标记);黑名单
|
|
|
712
733
|
{ "safeMode": { "blacklist": [".secrets/"] } }
|
|
713
734
|
`;
|
|
714
735
|
function parseRegistry(raw) {
|
|
715
|
-
const data = JSON.parse(raw);
|
|
736
|
+
const data = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
716
737
|
if (Array.isArray(data)) return data;
|
|
717
738
|
const entries = data.entries;
|
|
718
739
|
if (!Array.isArray(entries)) throw new Error("invalid registry: missing entries[]");
|
|
@@ -791,7 +812,7 @@ function runMsm(root, args) {
|
|
|
791
812
|
],
|
|
792
813
|
env: buildMsmEnv(root)
|
|
793
814
|
});
|
|
794
|
-
if (r.error && r.error
|
|
815
|
+
if (r.error && isBunMissing(r.error)) r = spawnSync(NPX_BIN, [
|
|
795
816
|
"tsx",
|
|
796
817
|
entry.path,
|
|
797
818
|
...businessArgs
|
|
@@ -1037,6 +1058,15 @@ function msmExecResult(name, status, stdout, stderr, fmtJson, hasHelp = false) {
|
|
|
1037
1058
|
* 用 execFile + promisify + timeout(超时自动 kill),**不阻塞 Node 事件循环**。
|
|
1038
1059
|
* (同步 spawnSync 版会阻塞 web 事件循环 → MSM 脚本自请求 3080 时死锁,见 postmortem。)
|
|
1039
1060
|
*/
|
|
1061
|
+
/** bun 缺失的错误码集(Windows 兼容:无 bun 时 execFile('bun') 抛 EINVAL 而非 ENOENT,见 Windows 审计问题 5) */
|
|
1062
|
+
const BUN_MISSING_CODES = /* @__PURE__ */ new Set([
|
|
1063
|
+
"ENOENT",
|
|
1064
|
+
"EINVAL",
|
|
1065
|
+
"EPERM"
|
|
1066
|
+
]);
|
|
1067
|
+
function isBunMissing(err) {
|
|
1068
|
+
return typeof err.code === "string" && BUN_MISSING_CODES.has(err.code);
|
|
1069
|
+
}
|
|
1040
1070
|
async function runMsmAsync(root, args) {
|
|
1041
1071
|
if (args.action !== "exec") return runMsm(root, args);
|
|
1042
1072
|
const { entry, businessArgs, fmtJson, hasHelp, protocol } = prepareExec(root, args);
|
|
@@ -1053,7 +1083,7 @@ async function runMsmAsync(root, args) {
|
|
|
1053
1083
|
return msmExecResult(entry.name, 0, r.stdout, r.stderr, fmtJson, hasHelp);
|
|
1054
1084
|
} catch (e) {
|
|
1055
1085
|
const err = e;
|
|
1056
|
-
if (err
|
|
1086
|
+
if (isBunMissing(err)) try {
|
|
1057
1087
|
const r = await execFileAsync(NPX_BIN, [
|
|
1058
1088
|
"tsx",
|
|
1059
1089
|
entry.path,
|
|
@@ -1221,6 +1251,12 @@ function sessionMdTemplate(title, id, goal, now) {
|
|
|
1221
1251
|
return `# SESSION: ${title}\n- ID: ${id}\n\n## 目标\n${goal ?? "(待补充)"}\n\n## 状态\n- [ ] 进行中\n\n## 关键决策\n| # | 决策 | 理由 |\n|---|------|------|\n| 1 | | |\n\n## 进度记录\n- ${ts} — 创建\n\n## 产出物\n- \n\n## 未解决的问题\n- \n`;
|
|
1222
1252
|
}
|
|
1223
1253
|
/** create 子命令(对齐 osp createSession:--desc/--issue 二选一 + dry-run + 长度限制) */
|
|
1254
|
+
/** 目录名脱敏(Windows 审计问题 10):非法字符 → '-', 去尾点/空格, 保留名(CON/NUL 等)加前缀 */
|
|
1255
|
+
function sanitizeDirName(s) {
|
|
1256
|
+
const cleaned = s.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "");
|
|
1257
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(cleaned)) return `_${cleaned}`;
|
|
1258
|
+
return cleaned;
|
|
1259
|
+
}
|
|
1224
1260
|
function createSession(opts) {
|
|
1225
1261
|
const { root, desc, issue, goal, dryRun } = opts;
|
|
1226
1262
|
const sessionsDir = sessionsRoot(root);
|
|
@@ -1230,7 +1266,7 @@ function createSession(opts) {
|
|
|
1230
1266
|
if (desc && issue) throw new Error("--desc and --issue are mutually exclusive");
|
|
1231
1267
|
if (issue) {
|
|
1232
1268
|
if (issue.length > 100) throw new Error(`issue too long: ${issue.length} chars (max 100)`);
|
|
1233
|
-
const dirName = `${datePrefix}--${issue}`;
|
|
1269
|
+
const dirName = `${datePrefix}--${sanitizeDirName(issue)}`;
|
|
1234
1270
|
const sessionPath = join(sessionsDir, dirName);
|
|
1235
1271
|
if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
|
|
1236
1272
|
if (dryRun) return {
|
|
@@ -1260,7 +1296,7 @@ function createSession(opts) {
|
|
|
1260
1296
|
}
|
|
1261
1297
|
}
|
|
1262
1298
|
const nextId = String(maxId + 1).padStart(3, "0");
|
|
1263
|
-
const dirName = `${datePrefix}--S${nextId}--${desc}`;
|
|
1299
|
+
const dirName = `${datePrefix}--S${nextId}--${sanitizeDirName(desc)}`;
|
|
1264
1300
|
const sessionPath = join(sessionsDir, dirName);
|
|
1265
1301
|
if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
|
|
1266
1302
|
if (dryRun) return {
|
|
@@ -1360,6 +1396,7 @@ function closeSession(root, key, confirm, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
1360
1396
|
const mdPath = join(session.path, SESSION_MD);
|
|
1361
1397
|
if (!existsSync(mdPath)) throw new Error(`Session "${session.dirName}" has no SESSION.md — nothing to close.`);
|
|
1362
1398
|
let content = readFileSync(mdPath, "utf-8");
|
|
1399
|
+
content = content.replace(/\r\n/g, "\n");
|
|
1363
1400
|
content = content.replace(/## 状态\n\n?- \[ \] 进行中/, "## 状态\n- [x] 已完成\n- [x] 已关闭");
|
|
1364
1401
|
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
1365
1402
|
if (!content.includes("-- 关闭")) content = content.replace(/(## 进度记录\n)/, `$1- ${now} — 关闭\n`);
|
|
@@ -1882,11 +1919,12 @@ function decideGuard(input) {
|
|
|
1882
1919
|
kind: "deny"
|
|
1883
1920
|
};
|
|
1884
1921
|
if (pathArg !== void 0) {
|
|
1885
|
-
const
|
|
1886
|
-
if (
|
|
1922
|
+
const abs = resolve(root, pathArg);
|
|
1923
|
+
if (!pathInside(resolve(root), abs)) return {
|
|
1887
1924
|
deny: `path escape blocked: "${pathArg}" 越出 CCC 根`,
|
|
1888
1925
|
kind: "deny"
|
|
1889
1926
|
};
|
|
1927
|
+
const rel = relative(root, abs).split("\\").join("/");
|
|
1890
1928
|
if (rel === ".serenity-safe-on" || rel === ".serenity" || rel.startsWith(".serenity-safe-on/") || rel.startsWith(".serenity/")) return {
|
|
1891
1929
|
deny: `CCC 治理文件 "${rel}" 保留给用户,agent 不可写`,
|
|
1892
1930
|
kind: "deny"
|
|
@@ -2018,14 +2056,21 @@ function registerGuards(ctx, opts = {}) {
|
|
|
2018
2056
|
* WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / loop 模型。
|
|
2019
2057
|
* setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
|
|
2020
2058
|
*/
|
|
2021
|
-
/**
|
|
2059
|
+
/**
|
|
2060
|
+
* 读取已安装 DSH CLI 版本;读不到返回 null。
|
|
2061
|
+
* 跨平台(Windows 审计问题 13):Windows npm 全局装在 %APPDATA%\npm(非 ~/.npm-global)——
|
|
2062
|
+
* 依次尝试 npm_config_prefix / APPDATA\npm / ~/.npm-global。
|
|
2063
|
+
*/
|
|
2022
2064
|
function readDshVersion() {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2065
|
+
const candidates = [];
|
|
2066
|
+
if (process.env.npm_config_prefix) candidates.push(join(process.env.npm_config_prefix, "lib", "node_modules", "@deepseek-ai", "dsh"));
|
|
2067
|
+
if (process.env.APPDATA) candidates.push(join(process.env.APPDATA, "npm", "node_modules", "@deepseek-ai", "dsh"));
|
|
2068
|
+
candidates.push(join(homedir(), ".npm-global", "lib", "node_modules", "@deepseek-ai", "dsh"));
|
|
2069
|
+
for (const p of candidates) try {
|
|
2070
|
+
const pkg = JSON.parse(readFileSync(join(p, "package.json"), "utf-8"));
|
|
2071
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
2072
|
+
} catch {}
|
|
2073
|
+
return null;
|
|
2029
2074
|
}
|
|
2030
2075
|
function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
2031
2076
|
const root = findSerenityRoot(cwd);
|
|
@@ -2267,7 +2312,7 @@ function readAll(root) {
|
|
|
2267
2312
|
const p = localstorePath(root);
|
|
2268
2313
|
if (!existsSync(p)) return {};
|
|
2269
2314
|
try {
|
|
2270
|
-
const v = JSON.parse(readFileSync(p, "utf-8"));
|
|
2315
|
+
const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
|
|
2271
2316
|
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
2272
2317
|
return {};
|
|
2273
2318
|
} catch {
|
|
@@ -2481,6 +2526,9 @@ const GIT_ACTIONS = [
|
|
|
2481
2526
|
"pull",
|
|
2482
2527
|
"diff"
|
|
2483
2528
|
];
|
|
2529
|
+
/** git 操作超时(ms)——网络路径(push/pull/fetch)可能挂起(GCM 弹认证框等),
|
|
2530
|
+
* 无 timeout 会冻结 Node 事件循环 / DSH 3080 server(Windows 审计问题 12) */
|
|
2531
|
+
const GIT_TIMEOUT_MS = 3e4;
|
|
2484
2532
|
function git(root, args) {
|
|
2485
2533
|
try {
|
|
2486
2534
|
return {
|
|
@@ -2492,14 +2540,15 @@ function git(root, args) {
|
|
|
2492
2540
|
"pipe",
|
|
2493
2541
|
"pipe"
|
|
2494
2542
|
],
|
|
2495
|
-
maxBuffer: 1048576
|
|
2543
|
+
maxBuffer: 1048576,
|
|
2544
|
+
timeout: GIT_TIMEOUT_MS
|
|
2496
2545
|
}).trimEnd(),
|
|
2497
2546
|
stderr: ""
|
|
2498
2547
|
};
|
|
2499
2548
|
} catch (err) {
|
|
2500
2549
|
return {
|
|
2501
2550
|
stdout: (err.stdout?.toString() ?? "").trimEnd(),
|
|
2502
|
-
stderr: (err.stderr?.toString() ?? "").trimEnd()
|
|
2551
|
+
stderr: err.killed ? `git 操作超时(${GIT_TIMEOUT_MS / 1e3}s)` : (err.stderr?.toString() ?? "").trimEnd()
|
|
2503
2552
|
};
|
|
2504
2553
|
}
|
|
2505
2554
|
}
|
|
@@ -3004,11 +3053,16 @@ function loopPresetInheritance(parentCtx) {
|
|
|
3004
3053
|
* 对齐 opencode-serenity-plugin 老 loop 语义:进度文件(loop-<label>.md/.json)、
|
|
3005
3054
|
* 续跑、轮次 prompt 结构、stop token。
|
|
3006
3055
|
*/
|
|
3056
|
+
/** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长 */
|
|
3057
|
+
function sanitizeLabel(label) {
|
|
3058
|
+
return label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "").slice(0, 50);
|
|
3059
|
+
}
|
|
3007
3060
|
function loopProgressPaths(root, label) {
|
|
3008
3061
|
const dir = join(root, "AGENT_SESSIONS");
|
|
3062
|
+
const safe = sanitizeLabel(label);
|
|
3009
3063
|
return {
|
|
3010
|
-
md: join(dir, `loop-${
|
|
3011
|
-
json: join(dir, `loop-${
|
|
3064
|
+
md: join(dir, `loop-${safe}.md`),
|
|
3065
|
+
json: join(dir, `loop-${safe}.json`)
|
|
3012
3066
|
};
|
|
3013
3067
|
}
|
|
3014
3068
|
/** 读取进度(续跑);无文件返回 round 0 */
|
|
@@ -3450,6 +3504,257 @@ const localstoreTool = defineTool({
|
|
|
3450
3504
|
}
|
|
3451
3505
|
});
|
|
3452
3506
|
//#endregion
|
|
3507
|
+
//#region src/seams/bootstrap.ts
|
|
3508
|
+
/** 默认首请求工具集:dsp 平台 Minimal 等价核心(anchored 是 bash+str_replace_editor) */
|
|
3509
|
+
const DEFAULT_BOOTSTRAP_TOOLS = [
|
|
3510
|
+
"read",
|
|
3511
|
+
"write",
|
|
3512
|
+
"edit",
|
|
3513
|
+
"glob",
|
|
3514
|
+
"grep"
|
|
3515
|
+
];
|
|
3516
|
+
/** 默认剥离的自动注入源(anchored 默认:可用技能目录提醒 + 工作区指令摘要) */
|
|
3517
|
+
const DEFAULT_SUPPRESSED_SOURCES = ["skill-catalog", "agent-instructions"];
|
|
3518
|
+
/** 默认 compaction 恢复工作集(anchored 默认 read/write/edit/glob/grep/todo_write/ask_user_question) */
|
|
3519
|
+
const DEFAULT_COMPACTION_TOOLS = [
|
|
3520
|
+
"read",
|
|
3521
|
+
"write",
|
|
3522
|
+
"edit",
|
|
3523
|
+
"glob",
|
|
3524
|
+
"grep",
|
|
3525
|
+
"todo_write"
|
|
3526
|
+
];
|
|
3527
|
+
/** 默认首轮锚定问题(用户指定:介绍宁静号,200 字以内) */
|
|
3528
|
+
const DEFAULT_ANCHOR_MESSAGE = "请介绍当前宁静号,它是什么,为了什么,200字以内回答";
|
|
3529
|
+
/** 构建一个 epoch 感知晋升跟踪器(纯逻辑,可单测) */
|
|
3530
|
+
function createEpochPromotion(promoteEvents) {
|
|
3531
|
+
/** sessionId -> { boundary, promoted } */
|
|
3532
|
+
const state = /* @__PURE__ */ new Map();
|
|
3533
|
+
const sessionIdOf = (session) => {
|
|
3534
|
+
if (session && typeof session === "object") {
|
|
3535
|
+
const id = session.id;
|
|
3536
|
+
if (typeof id === "string") return id;
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
const scan = (session) => {
|
|
3540
|
+
let boundary = -1;
|
|
3541
|
+
let promoted = false;
|
|
3542
|
+
const events = session?.events;
|
|
3543
|
+
if (Array.isArray(events)) for (const event of events) {
|
|
3544
|
+
const e = event;
|
|
3545
|
+
const seq = typeof e.seq === "number" ? e.seq : 0;
|
|
3546
|
+
if (e.type === "compaction/end") {
|
|
3547
|
+
boundary = seq;
|
|
3548
|
+
promoted = false;
|
|
3549
|
+
continue;
|
|
3550
|
+
}
|
|
3551
|
+
if (promoteEvents.has(e.type) && seq > boundary) promoted = true;
|
|
3552
|
+
}
|
|
3553
|
+
const entry = {
|
|
3554
|
+
boundary,
|
|
3555
|
+
promoted
|
|
3556
|
+
};
|
|
3557
|
+
const sid = sessionIdOf(session);
|
|
3558
|
+
if (sid) state.set(sid, entry);
|
|
3559
|
+
return entry;
|
|
3560
|
+
};
|
|
3561
|
+
return {
|
|
3562
|
+
status(agent) {
|
|
3563
|
+
if (agent === void 0) return {
|
|
3564
|
+
boundary: -1,
|
|
3565
|
+
promoted: true
|
|
3566
|
+
};
|
|
3567
|
+
const session = agent.session;
|
|
3568
|
+
if (session === void 0) return {
|
|
3569
|
+
boundary: -1,
|
|
3570
|
+
promoted: true
|
|
3571
|
+
};
|
|
3572
|
+
if ((session.header?.delegationDepth ?? 0) > 0) return {
|
|
3573
|
+
boundary: -1,
|
|
3574
|
+
promoted: true
|
|
3575
|
+
};
|
|
3576
|
+
const sid = sessionIdOf(session);
|
|
3577
|
+
if (sid === void 0) return {
|
|
3578
|
+
boundary: -1,
|
|
3579
|
+
promoted: true
|
|
3580
|
+
};
|
|
3581
|
+
return state.get(sid) ?? scan(session);
|
|
3582
|
+
},
|
|
3583
|
+
observe(session, event) {
|
|
3584
|
+
const sid = sessionIdOf(session);
|
|
3585
|
+
if (sid === void 0) return;
|
|
3586
|
+
const entry = state.get(sid);
|
|
3587
|
+
if (entry === void 0) return;
|
|
3588
|
+
const e = event;
|
|
3589
|
+
const seq = typeof e.seq === "number" ? e.seq : 0;
|
|
3590
|
+
if (e.type === "compaction/end") {
|
|
3591
|
+
state.set(sid, {
|
|
3592
|
+
boundary: seq,
|
|
3593
|
+
promoted: false
|
|
3594
|
+
});
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
if (promoteEvents.has(e.type) && seq > entry.boundary && !entry.promoted) state.set(sid, {
|
|
3598
|
+
...entry,
|
|
3599
|
+
promoted: true
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
const PROMOTE_EVENTS = {
|
|
3605
|
+
"tool-call": /* @__PURE__ */ new Set(["tool/call"]),
|
|
3606
|
+
"assistant-message": /* @__PURE__ */ new Set(["assistant/message"]),
|
|
3607
|
+
either: /* @__PURE__ */ new Set(["tool/call", "assistant/message"])
|
|
3608
|
+
};
|
|
3609
|
+
function stringList(value, field, fallback) {
|
|
3610
|
+
if (value === void 0) return [...fallback];
|
|
3611
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new TypeError(`bootstrap: ${field} must be a non-empty array of non-empty strings`);
|
|
3612
|
+
return [...new Set(value)];
|
|
3613
|
+
}
|
|
3614
|
+
function sourceList(value, field, fallback) {
|
|
3615
|
+
if (value === void 0) return new Set(fallback);
|
|
3616
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new TypeError(`bootstrap: ${field} must be an array of non-empty strings`);
|
|
3617
|
+
return new Set(value);
|
|
3618
|
+
}
|
|
3619
|
+
function resolveBootstrapSettings(opts) {
|
|
3620
|
+
const promoteOn = opts.promoteOn ?? "either";
|
|
3621
|
+
if (promoteOn !== "either" && promoteOn !== "tool-call" && promoteOn !== "assistant-message") throw new TypeError(`bootstrap: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(promoteOn)}`);
|
|
3622
|
+
return {
|
|
3623
|
+
bootstrapTools: stringList(opts.bootstrapTools, "bootstrapTools", DEFAULT_BOOTSTRAP_TOOLS),
|
|
3624
|
+
promoteEvents: PROMOTE_EVENTS[promoteOn],
|
|
3625
|
+
suppressedSources: sourceList(opts.suppressedContextSources, "suppressedContextSources", DEFAULT_SUPPRESSED_SOURCES),
|
|
3626
|
+
compactionTools: stringList(opts.compactionTools, "compactionTools", DEFAULT_COMPACTION_TOOLS),
|
|
3627
|
+
anchorMessage: typeof opts.anchorMessage === "string" && opts.anchorMessage.length > 0 ? opts.anchorMessage : DEFAULT_ANCHOR_MESSAGE
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* 注册 Anchored bootstrap 机制(按 agent cwd 动态解析 CCC 配置——多 CCC 架构,与 guards 同模式):
|
|
3632
|
+
* 1. session/event 观察晋升信号(tool/call + assistant/message + compaction/end)
|
|
3633
|
+
* 2. system-prompt/assemble:bootstrap 阶段目录窄化到 bootstrapTools(+compaction 后 compactionTools)
|
|
3634
|
+
* 3. agent/pre-step:bootstrap 阶段剥离 suppressedSources 注入消息(降级保留全部)
|
|
3635
|
+
*
|
|
3636
|
+
* CCC 的 serenity.json 配置 `bootstrap.enabled: true` 才生效(否则零影响);
|
|
3637
|
+
* 摘除 = 删除 index.ts 中本注册行(独立模块)。
|
|
3638
|
+
*/
|
|
3639
|
+
/** 从 agent 解析 CCC 根(宽松;无 agent/无 CCC 返回 null) */
|
|
3640
|
+
function agentRoot(agent) {
|
|
3641
|
+
const cwd = agent?.session?.header?.cwd;
|
|
3642
|
+
if (typeof cwd !== "string") return null;
|
|
3643
|
+
return findSerenityRoot(cwd);
|
|
3644
|
+
}
|
|
3645
|
+
/** 按 agent cwd 解析 bootstrap 配置;未开启返回 null */
|
|
3646
|
+
function readBootstrapConfig(agent) {
|
|
3647
|
+
const root = agentRoot(agent);
|
|
3648
|
+
if (!root) return null;
|
|
3649
|
+
const b = loadSerenityConfig(root).bootstrap;
|
|
3650
|
+
if (!b?.enabled) return null;
|
|
3651
|
+
return resolveBootstrapSettings({
|
|
3652
|
+
bootstrapTools: b.bootstrapTools,
|
|
3653
|
+
promoteOn: b.promoteOn,
|
|
3654
|
+
suppressedContextSources: b.suppressedContextSources,
|
|
3655
|
+
compactionTools: b.compactionTools,
|
|
3656
|
+
anchorMessage: b.anchorMessage
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
function registerBootstrap(ctx) {
|
|
3660
|
+
const settingsByRoot = /* @__PURE__ */ new Map();
|
|
3661
|
+
const tracker = createEpochPromotion(/* @__PURE__ */ new Set(["tool/call", "assistant/message"]));
|
|
3662
|
+
ctx.on("session/event", (session, event) => {
|
|
3663
|
+
try {
|
|
3664
|
+
tracker.observe(session, event);
|
|
3665
|
+
} catch {}
|
|
3666
|
+
});
|
|
3667
|
+
ctx.on("agent/inbox/inserted", ({ agent, message }) => {
|
|
3668
|
+
try {
|
|
3669
|
+
const root = agentRoot(agent);
|
|
3670
|
+
if (!root) return;
|
|
3671
|
+
const settings = settingsByRoot.get(root) ?? readBootstrapConfig(agent);
|
|
3672
|
+
if (!settings) return;
|
|
3673
|
+
const session = agent.session;
|
|
3674
|
+
if ((session?.header?.delegationDepth ?? 0) > 0) return;
|
|
3675
|
+
if (session?.events?.some((event) => event.type === "user/message")) return;
|
|
3676
|
+
if (message?.source?.kind === "plugin") return;
|
|
3677
|
+
const inbox = agent.inbox;
|
|
3678
|
+
if (!inbox?.prepend) return;
|
|
3679
|
+
inbox.prepend("next-turn", {
|
|
3680
|
+
id: `bootstrap-anchor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3681
|
+
role: "user",
|
|
3682
|
+
content: [{
|
|
3683
|
+
type: "text",
|
|
3684
|
+
text: settings.anchorMessage
|
|
3685
|
+
}],
|
|
3686
|
+
source: {
|
|
3687
|
+
kind: "plugin",
|
|
3688
|
+
plugin: "dsh-serenity-hooks",
|
|
3689
|
+
form: "notice",
|
|
3690
|
+
summary: "bootstrap anchor turn"
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3693
|
+
warnOnce(`anchor turn injected: "${settings.anchorMessage.slice(0, 40)}…"`);
|
|
3694
|
+
} catch {}
|
|
3695
|
+
});
|
|
3696
|
+
let warned = false;
|
|
3697
|
+
const warnOnce = (message) => {
|
|
3698
|
+
if (warned) return;
|
|
3699
|
+
warned = true;
|
|
3700
|
+
try {
|
|
3701
|
+
console.warn(`[serenity-hooks] bootstrap: ${message}`);
|
|
3702
|
+
} catch {}
|
|
3703
|
+
};
|
|
3704
|
+
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
3705
|
+
const assembled = await next();
|
|
3706
|
+
try {
|
|
3707
|
+
const agent = context.agent;
|
|
3708
|
+
const root = agentRoot(agent);
|
|
3709
|
+
if (!root) return assembled;
|
|
3710
|
+
const settings = settingsByRoot.get(root) ?? readBootstrapConfig(agent);
|
|
3711
|
+
if (!settings) return assembled;
|
|
3712
|
+
settingsByRoot.set(root, settings);
|
|
3713
|
+
const status = tracker.status(agent);
|
|
3714
|
+
if (status.promoted) return assembled;
|
|
3715
|
+
const keep = new Set(settings.bootstrapTools);
|
|
3716
|
+
if (status.boundary >= 0) for (const toolName of settings.compactionTools) keep.add(toolName);
|
|
3717
|
+
const tools = assembled.tools;
|
|
3718
|
+
if (!Array.isArray(tools)) return assembled;
|
|
3719
|
+
const available = new Set(tools.map((tool) => tool.name).filter((n) => typeof n === "string"));
|
|
3720
|
+
const missing = [...keep].filter((name) => !available.has(name));
|
|
3721
|
+
if (missing.length > 0) {
|
|
3722
|
+
warnOnce(`expected bootstrap tools missing=${JSON.stringify(missing)} — bootstrap disabled, full catalog exposed`);
|
|
3723
|
+
return assembled;
|
|
3724
|
+
}
|
|
3725
|
+
return {
|
|
3726
|
+
...assembled,
|
|
3727
|
+
tools: tools.filter((tool) => typeof tool.name === "string" && keep.has(tool.name))
|
|
3728
|
+
};
|
|
3729
|
+
} catch (error) {
|
|
3730
|
+
warnOnce(`assemble filter failed, exposing the full catalog: ${String(error?.message ?? error)}`);
|
|
3731
|
+
return assembled;
|
|
3732
|
+
}
|
|
3733
|
+
});
|
|
3734
|
+
ctx.on("agent/pre-step", async ({ agent }, next) => {
|
|
3735
|
+
const decision = await next();
|
|
3736
|
+
if (decision.kind === "reject") return decision;
|
|
3737
|
+
try {
|
|
3738
|
+
const settings = readBootstrapConfig(agent);
|
|
3739
|
+
if (!settings) return decision;
|
|
3740
|
+
if (tracker.status(agent).promoted || settings.suppressedSources.size === 0) return decision;
|
|
3741
|
+
const messages = decision.messages;
|
|
3742
|
+
if (!Array.isArray(messages)) return decision;
|
|
3743
|
+
const kept = messages.filter((message) => {
|
|
3744
|
+
const kind = message?.source?.kind;
|
|
3745
|
+
return typeof kind !== "string" || !settings.suppressedSources.has(kind);
|
|
3746
|
+
});
|
|
3747
|
+
return kept.length === messages.length ? decision : {
|
|
3748
|
+
...decision,
|
|
3749
|
+
messages: kept
|
|
3750
|
+
};
|
|
3751
|
+
} catch (error) {
|
|
3752
|
+
warnOnce(`pre-step context filter failed, keeping injected context: ${String(error?.message ?? error)}`);
|
|
3753
|
+
return decision;
|
|
3754
|
+
}
|
|
3755
|
+
}, { prepend: true });
|
|
3756
|
+
}
|
|
3757
|
+
//#endregion
|
|
3453
3758
|
//#region src/seams/loop.ts
|
|
3454
3759
|
/** 解析当前 dsh 会话 scope 的活跃会话 SESSION.md 绝对路径;无标记/越界返回 null */
|
|
3455
3760
|
function resolveActiveSession(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
@@ -4253,16 +4558,17 @@ function registerEnv(ctx) {
|
|
|
4253
4558
|
*/
|
|
4254
4559
|
/** 解析 --- 分隔的 YAML frontmatter(只需 name/description/whenToUse) */
|
|
4255
4560
|
function parseFrontmatter(raw) {
|
|
4256
|
-
const
|
|
4561
|
+
const text = raw.replace(/^\uFEFF/, "");
|
|
4562
|
+
const m = /^---\s*\n([\s\S]*?)\n---\s*\n?/.exec(text);
|
|
4257
4563
|
if (!m) return {
|
|
4258
4564
|
meta: {
|
|
4259
4565
|
name: "",
|
|
4260
4566
|
description: ""
|
|
4261
4567
|
},
|
|
4262
|
-
content:
|
|
4568
|
+
content: text
|
|
4263
4569
|
};
|
|
4264
4570
|
const fm = m[1];
|
|
4265
|
-
const body =
|
|
4571
|
+
const body = text.slice(m[0].length);
|
|
4266
4572
|
const grab = (key) => {
|
|
4267
4573
|
const line = fm.split("\n").find((l) => l.startsWith(`${key}:`));
|
|
4268
4574
|
if (!line) return void 0;
|
|
@@ -4380,7 +4686,8 @@ const Config = z.object({
|
|
|
4380
4686
|
api: z.boolean().default(true),
|
|
4381
4687
|
entrySkillMaxChars: z.number().default(3e4),
|
|
4382
4688
|
env: z.boolean().default(true),
|
|
4383
|
-
opencodeSkills: z.boolean().default(true)
|
|
4689
|
+
opencodeSkills: z.boolean().default(true),
|
|
4690
|
+
bootstrap: z.boolean().default(false)
|
|
4384
4691
|
});
|
|
4385
4692
|
function apply(ctx, config) {
|
|
4386
4693
|
if (config.tools) {
|
|
@@ -4413,6 +4720,7 @@ function apply(ctx, config) {
|
|
|
4413
4720
|
if (config.api) registerStatusApi(ctx, { configPaths: config.serenityConfigPaths });
|
|
4414
4721
|
if (config.env) registerEnv(ctx);
|
|
4415
4722
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
4723
|
+
if (config.bootstrap) registerBootstrap(ctx);
|
|
4416
4724
|
}
|
|
4417
4725
|
//#endregion
|
|
4418
4726
|
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|