@shgroup/dsh-serenity-hooks 1.55.0 → 1.56.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/README.md CHANGED
@@ -76,7 +76,7 @@ dsh plugin --profile web add link:$(pwd)/hooks/dsh-serenity-hooks
76
76
  | `msm` | 小工具执行入口:`msm("名字", ["参数"])`;名字记不全就给候选;`inspect=true` 看用法 | 调用工作区里注册的任何小工具 |
77
77
  | `praxis` | 按需给 AI 注入四套"做事方法":输出自检(eap)、设计对齐(neat)、认知连续性(cce)、本体工程(ontology) | 要它把话说清楚 / 先对齐再动手 / 动手前先把领域结构显式化时 |
78
78
  | `ontology` | 本体工程**引擎**(能跑的那个):读 `.ttl` 本体文件,做结构校验(M1–M10,含公理表达式语法)、生成摘要、查声明与实况是否一致(V1–V4)、出语义审查提示词、出"公理 ↔ 技能"合规清单;还能写一份新本体的模板 | 想给某个领域建/校验本体时。⚠️ 与 `praxis ontology` 是**同一领域的两个面**:那个是**方法文本**(怎么建模),这个是**引擎**(真去检查)。路径必须在工作区内 |
79
- | `serenity-skill-creator` | 增强型 skill 标准(**serenity-skill**)的 **creator**:`guide`(编写与优化方法,先读它)/`create`(生成 skill 骨架 = SKILL.md + skill.ttl + references/,**基础链的相对路径由它算**)/`check`(**A1 结构 + A2 链上基础链**,三态可分辨:已建模通过/普通 skill/建模了但检查没跑成)/`review`(**C 档**:文字与本体逐条的合规问题清单,半机械,要人回答)/`index`(预览**模型实际会看到**的第二层参考件清单)/`status`(全容器一览:哪些已建模) | 新建或改造 skill、给 skill 建本体、想知道"模型到底看不看得见我那些参考件"时 |
79
+ | `serenity-skill-creator` | 增强型 skill 标准(**serenity-skill**)的 **creator**:`guide`(编写与优化方法,先读它)/`create`(生成 skill 骨架 = SKILL.md + skill.ttl + references/,**基础链的相对路径由它算**)/`check`(**A3 分层档**(**每个 skill 都查**:顶层非桩 / 顶层不自述"只是导航层" / references/ 一级 .md 全部被点名)+ **A1 结构 + A2 链上基础链**,三态可分辨:已建模通过/普通 skill/建模了但检查没跑成)/`review`(**C 档**:文字与本体逐条的合规问题清单,半机械,要人回答)/`index`(预览**模型实际会看到**的第二层参考件清单)/`status`(全容器一览:哪些已建模) | 新建或改造 skill、给 skill 建本体、想知道"模型到底看不看得见我那些参考件"时 |
80
80
  | `handyman` | 杂工:派一个便宜模型的助手干活。**默认模式(foreground)**= 一次串行委派、拿回结果;**background 模式**= 循环干活直到完成(完成码校验 / 轮次上限 / 自动重启 / 进度文件),也可一次派多个并行 | 大批量、重复性的活(扫描几十个技能、逐用例回归) |
81
81
  | `localstore` | 存密钥和配置(凭据、偏好两个命名空间) | API key、密码集中放一处,不进 git |
82
82
  | `container_admin` | 机务舱:管理子角色、管理小工具注册表、查看全部配置 | 定义"子角色"、注册新小工具、改配置时 |
@@ -158,6 +158,10 @@ my-workspace/ ← 工作区根目录(放一个 .serenity
158
158
  └── files_from_user/
159
159
  ```
160
160
 
161
+ > 🔴 **技能的两层披露**:技能目录里除了 `SKILL.md`,还可以有 `references/`(参考件)与 `skill.ttl`(该技能的本体约束)。
162
+ > 助手看到的是**两层**:**目录那一行就带参考件文件名**(插件按文件树即时生成 ⇒ 你新增一个文件它自己就更新,**不用手写**),
163
+ > 真要用时它才去 `read` 打开正文 —— **正文不进上下文,除非它自己去取**。
164
+
161
165
  ---
162
166
 
163
167
  ## 5. 能拿它做什么(12 个真实用例)
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.55.0",
3
+ "version": "1.56.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/handyman/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1(deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -2246,6 +2246,16 @@ function truncateContent(content, maxChars) {
2246
2246
  * `references/*-domain.ttl`(旧名)一律当**普通参考件**:不特殊对待、不告警 —— 那 14 份由 CCC 自行改名。
2247
2247
  * 🔴 「未建模」不是异常(裁决 Q6 / 判据 J9):无 `skill.ttl` 的**普通 skill** 照常产出索引,
2248
2248
  * **零告警、零误差** —— 增强必须对普通 skill 完全向后兼容。
2249
+ *
2250
+ * 🔴 **两个面,两个入口**(2026-10-12 增;owner 裁决 O2)—— 同源、但成本形态不同:
2251
+ * · **载入面** `buildResourceIndex()` —— 完整索引(名字 + 首标题 + 字节),带 `.md` 文件头读取;
2252
+ * 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`trajectory-skills.ts`
2253
+ * order −45)/目录 provider 的 `get()`(`opencode-skills.ts`)。
2254
+ * · **目录面** `buildResourceNameSuffix()` —— **只有名字**,**不读任何正文**;只挂一处:
2255
+ * 目录 provider 的 `list()`(`available_skills` 的 `SkillSummary.description`)。
2256
+ * 🔴 为什么目录面在这里:`SkillSummary` 契约里**没有列资源的位**(只有 name/description/whenToUse)
2257
+ * ⇒ `description` 是唯一位置,且必须**自动生成**。
2258
+ * ⇒ 两个面**共用** `collectEntries()`(范围 = 规则⑥只写一遍)。
2249
2259
  */
2250
2260
  /** 第二层占位口:本 skill 的本体约束文件(固定名,设计 §5.1) */
2251
2261
  const SKILL_ONTOLOGY_FILE = "skill.ttl";
@@ -2308,7 +2318,8 @@ function scanDirStats(dir) {
2308
2318
  capped
2309
2319
  };
2310
2320
  }
2311
- /** 按 UTF-8 **字节序**比较两个相对路径(规则②;不是 locale 比较、不是 mtime) */
2321
+ /** 按 UTF-8 **字节序**比较两个相对路径(规则②;不是 locale 比较、不是 mtime)。
2322
+ * 参数放宽到 `{ key }`:**未加水**的条目与**已加水**的条目共用同一个比较器 ⇒ 排序规则只写一遍。 */
2312
2323
  function byByteOrder(a, b) {
2313
2324
  return Buffer.compare(a.key, b.key);
2314
2325
  }
@@ -2354,25 +2365,15 @@ function firstHeading(file) {
2354
2365
  }
2355
2366
  return "";
2356
2367
  }
2357
- /** 收集候选(`references/` 一级条目 + 固定名本体),并按字节序排好 */
2358
- function collectItems(skillDir) {
2359
- const items = [];
2360
- const add = (rel, abs, ontology) => {
2361
- let bytes;
2362
- try {
2363
- bytes = statSync(abs).size;
2364
- } catch {
2365
- return;
2366
- }
2367
- items.push({
2368
- rel,
2369
- kind: "file",
2370
- bytes,
2371
- excerpt: rel.endsWith(".md") ? firstHeading(abs) : "",
2372
- ontology,
2373
- key: Buffer.from(rel, "utf-8")
2374
- });
2375
- };
2368
+ /**
2369
+ * 收集第二层的**条目**(范围 = 规则⑥:`references/` 一级条目 + 固定名本体)。
2370
+ *
2371
+ * 🔴 **只做 `readdir` / `stat`,不读任何文件正文** —— 两个消费者(第二层索引 / 目录面的名字清单)
2372
+ * **共用这一份范围定义**:规则⑥只该有一个家,各写一遍必然漂移。
2373
+ * 🔴 返回已按**字节序**排好(规则②:确定性,不用 mtime)。
2374
+ */
2375
+ function collectEntries(skillDir) {
2376
+ const entries = [];
2376
2377
  const refDir = join(skillDir, REFERENCE_DIR);
2377
2378
  if (existsSync(refDir)) {
2378
2379
  let names = [];
@@ -2390,24 +2391,64 @@ function collectItems(skillDir) {
2390
2391
  continue;
2391
2392
  }
2392
2393
  const rel = `${REFERENCE_DIR}/${name}`;
2393
- if (isDir) {
2394
- const { files, bytes, capped } = scanDirStats(abs);
2395
- items.push({
2396
- rel,
2397
- kind: "dir",
2398
- bytes,
2399
- excerpt: `子目录 · ${capped ? `≥${groupThousands(files)}` : groupThousands(files)} 个文件`,
2400
- ontology: false,
2401
- key: Buffer.from(rel, "utf-8")
2402
- });
2403
- continue;
2404
- }
2405
- add(rel, abs, false);
2394
+ entries.push({
2395
+ rel,
2396
+ kind: isDir ? "dir" : "file",
2397
+ abs,
2398
+ ontology: false,
2399
+ key: Buffer.from(rel, "utf-8")
2400
+ });
2406
2401
  }
2407
2402
  }
2408
2403
  const ontoAbs = join(skillDir, SKILL_ONTOLOGY_FILE);
2409
- if (existsSync(ontoAbs)) add(SKILL_ONTOLOGY_FILE, ontoAbs, true);
2410
- items.sort(byByteOrder);
2404
+ if (existsSync(ontoAbs)) entries.push({
2405
+ rel: SKILL_ONTOLOGY_FILE,
2406
+ kind: "file",
2407
+ abs: ontoAbs,
2408
+ ontology: true,
2409
+ key: Buffer.from(SKILL_ONTOLOGY_FILE, "utf-8")
2410
+ });
2411
+ entries.sort(byByteOrder);
2412
+ return entries;
2413
+ }
2414
+ /**
2415
+ * 给条目**加水**(字节 + 摘要)—— 这两项都要读盘,因此与"有哪些条目"分开。
2416
+ * 读不到 ⇒ `null`(宁可少列,也不列出读不到的项)。
2417
+ */
2418
+ function hydrate(entry) {
2419
+ if (entry.kind === "dir") {
2420
+ const { files, bytes, capped } = scanDirStats(entry.abs);
2421
+ return {
2422
+ rel: entry.rel,
2423
+ kind: "dir",
2424
+ bytes,
2425
+ excerpt: `子目录 · ${capped ? `≥${groupThousands(files)}` : groupThousands(files)} 个文件`,
2426
+ ontology: false,
2427
+ key: entry.key
2428
+ };
2429
+ }
2430
+ let bytes;
2431
+ try {
2432
+ bytes = statSync(entry.abs).size;
2433
+ } catch {
2434
+ return null;
2435
+ }
2436
+ return {
2437
+ rel: entry.rel,
2438
+ kind: "file",
2439
+ bytes,
2440
+ excerpt: entry.rel.endsWith(".md") ? firstHeading(entry.abs) : "",
2441
+ ontology: entry.ontology,
2442
+ key: entry.key
2443
+ };
2444
+ }
2445
+ /** 收集候选(`references/` 一级条目 + 固定名本体),加好水并按字节序排好 */
2446
+ function collectItems(skillDir) {
2447
+ const items = [];
2448
+ for (const entry of collectEntries(skillDir)) {
2449
+ const item = hydrate(entry);
2450
+ if (item !== null) items.push(item);
2451
+ }
2411
2452
  return items;
2412
2453
  }
2413
2454
  /**
@@ -2439,6 +2480,35 @@ function buildResourceIndex(skillDir, options = {}) {
2439
2480
  if (body.length <= maxChars) return body;
2440
2481
  return `${body.slice(0, maxChars)}\n…(第二层索引已按 ${groupThousands(maxChars)} 字符上限截断,原文 ${groupThousands(body.length)} 字符)`;
2441
2482
  }
2483
+ /**
2484
+ * 收集**只有名字**的第二层清单(目录面专用)。
2485
+ *
2486
+ * 为什么单独一个入口(而不是复用 `buildResourceIndex`):**成本形态不同** ——
2487
+ * 索引要给每个 `.md` 读文件头取摘要(规则④),而目录面是**每请求 × 每个 skill** 都要算的
2488
+ * ⇒ 这条路径**一个字节正文都不读**(只 `readdir`/`stat`)。
2489
+ * 范围仍与索引**同源**(共用 `collectEntries` ⇒ 规则⑥只有一个家)。
2490
+ */
2491
+ function buildResourceNames(skillDir, options = {}) {
2492
+ const maxItems = options.maxItems ?? 40;
2493
+ const entries = collectEntries(skillDir);
2494
+ const shown = entries.slice(0, maxItems);
2495
+ return {
2496
+ names: shown.map((e) => e.kind === "dir" ? `${basename(e.rel)}/` : basename(e.rel)),
2497
+ hidden: entries.length - shown.length
2498
+ };
2499
+ }
2500
+ /**
2501
+ * 目录面后缀:把名字清单拼成**可直接接在 `description` 后面**的一段文本。
2502
+ *
2503
+ * 🔴 **自动生成,绝不手写**(手写必与文件树漂);无可列项 ⇒ `''`(**普通 skill 零噪声** —— 判据 J9)。
2504
+ * 🔴 **有界 + 响亮**(规则③):超上限时写出"另有 N 项",**绝不静默截断**。
2505
+ */
2506
+ function buildResourceNameSuffix(skillDir, options = {}) {
2507
+ const { names, hidden } = buildResourceNames(skillDir, options);
2508
+ if (names.length === 0) return "";
2509
+ const tail = hidden > 0 ? ` …另有 ${hidden} 项` : "";
2510
+ return `(参考件:${names.join(" / ")}${tail})`;
2511
+ }
2442
2512
  //#endregion
2443
2513
  //#region src/trajectory-skills.ts
2444
2514
  /**
@@ -14773,9 +14843,11 @@ function registerOpencodeSkills(ctx) {
14773
14843
  for (const dir of listOpencodeSkillDirs(root)) {
14774
14844
  const skill = loadOpencodeSkill(dir);
14775
14845
  if (!skill || !skill.meta.name) continue;
14846
+ const baseDesc = skill.meta.description || "(opencode skill)";
14847
+ const nameSuffix = buildResourceNameSuffix(dir);
14776
14848
  candidates.push({
14777
14849
  name: skill.meta.name,
14778
- description: skill.meta.description || "(opencode skill)",
14850
+ description: nameSuffix ? `${baseDesc}${nameSuffix}` : baseDesc,
14779
14851
  ...skill.meta.whenToUse ? { whenToUse: skill.meta.whenToUse } : {},
14780
14852
  invocation: {
14781
14853
  modelInvocable: true,
@@ -7,6 +7,11 @@
7
7
  * 使 references/scripts 相对引用可解析。
8
8
  *
9
9
  * rank = 250:低于 project-dsh(100)与 custom(300)之间——本地 .dsh/skills 优先。
10
+ *
11
+ * 🔴 **本 provider 是第二层披露树的第四条落点所在**(2026-10-12,owner 裁决 O2):
12
+ * · `list()` = **目录面**(`available_skills` 的 `SkillSummary`)⇒ 参考件只能挂在 `description` 上(**只有名字**);
13
+ * · `get()` = **载入面**(`SkillDefinition.content`)⇒ 挂**完整索引**(名字 + 首标题 + 字节)。
14
+ * 两处**同源**(`src/skills/resource-index.ts`),不是各写一遍。
10
15
  */
11
16
  import type { Context } from 'cordis';
12
17
  export declare const OPENCODE_PROVIDER = "opencode-skills";
@@ -27,6 +27,16 @@
27
27
  * `references/*-domain.ttl`(旧名)一律当**普通参考件**:不特殊对待、不告警 —— 那 14 份由 CCC 自行改名。
28
28
  * 🔴 「未建模」不是异常(裁决 Q6 / 判据 J9):无 `skill.ttl` 的**普通 skill** 照常产出索引,
29
29
  * **零告警、零误差** —— 增强必须对普通 skill 完全向后兼容。
30
+ *
31
+ * 🔴 **两个面,两个入口**(2026-10-12 增;owner 裁决 O2)—— 同源、但成本形态不同:
32
+ * · **载入面** `buildResourceIndex()` —— 完整索引(名字 + 首标题 + 字节),带 `.md` 文件头读取;
33
+ * 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`trajectory-skills.ts`
34
+ * order −45)/目录 provider 的 `get()`(`opencode-skills.ts`)。
35
+ * · **目录面** `buildResourceNameSuffix()` —— **只有名字**,**不读任何正文**;只挂一处:
36
+ * 目录 provider 的 `list()`(`available_skills` 的 `SkillSummary.description`)。
37
+ * 🔴 为什么目录面在这里:`SkillSummary` 契约里**没有列资源的位**(只有 name/description/whenToUse)
38
+ * ⇒ `description` 是唯一位置,且必须**自动生成**。
39
+ * ⇒ 两个面**共用** `collectEntries()`(范围 = 规则⑥只写一遍)。
30
40
  */
31
41
  /** 第二层占位口:本 skill 的本体约束文件(固定名,设计 §5.1) */
32
42
  export declare const SKILL_ONTOLOGY_FILE = "skill.ttl";
@@ -62,3 +72,26 @@ export declare function hasSkillOntology(skillDir: string): boolean;
62
72
  * ⇒ 整块不产出 —— 空标题是噪声)。调用方按"空串不产出该块"处理。
63
73
  */
64
74
  export declare function buildResourceIndex(skillDir: string, options?: ResourceIndexOptions): string;
75
+ /** 目录面(`SkillSummary.description`)用的名字清单 —— **只有名字**,无摘要、无字节 */
76
+ export interface ResourceNameList {
77
+ /** 条目的**基名**(目录带尾 `/`;本体文件就是 `skill.ttl`),按字节序 */
78
+ names: string[];
79
+ /** 因条数上限**没列出**的条数(`> 0` ⇒ 调用方必须**响亮**写出,规则③) */
80
+ hidden: number;
81
+ }
82
+ /**
83
+ * 收集**只有名字**的第二层清单(目录面专用)。
84
+ *
85
+ * 为什么单独一个入口(而不是复用 `buildResourceIndex`):**成本形态不同** ——
86
+ * 索引要给每个 `.md` 读文件头取摘要(规则④),而目录面是**每请求 × 每个 skill** 都要算的
87
+ * ⇒ 这条路径**一个字节正文都不读**(只 `readdir`/`stat`)。
88
+ * 范围仍与索引**同源**(共用 `collectEntries` ⇒ 规则⑥只有一个家)。
89
+ */
90
+ export declare function buildResourceNames(skillDir: string, options?: ResourceIndexOptions): ResourceNameList;
91
+ /**
92
+ * 目录面后缀:把名字清单拼成**可直接接在 `description` 后面**的一段文本。
93
+ *
94
+ * 🔴 **自动生成,绝不手写**(手写必与文件树漂);无可列项 ⇒ `''`(**普通 skill 零噪声** —— 判据 J9)。
95
+ * 🔴 **有界 + 响亮**(规则③):超上限时写出"另有 N 项",**绝不静默截断**。
96
+ */
97
+ export declare function buildResourceNameSuffix(skillDir: string, options?: ResourceIndexOptions): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.55.0",
3
+ "version": "1.56.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:13 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/ontology/handyman/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1。",
5
5
  "license": "MIT",
6
6
  "repository": {