@shgroup/dsh-serenity-hooks 1.43.0 → 1.44.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.
@@ -1,3 +1,21 @@
1
+ import { dirname, join } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/constants.ts
5
+ /** 常量(纯模块,零 DSH 依赖) */
6
+ /**
7
+ * ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
8
+ * 发布时只需改 package.json 的 version。
9
+ */
10
+ const ACC_VERSION = (() => {
11
+ try {
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8")).version ?? "0.0.0";
14
+ } catch {
15
+ return "0.0.0";
16
+ }
17
+ })();
18
+ //#endregion
1
19
  //#region src/skiff-registry.ts
2
20
  const skiffSessions = /* @__PURE__ */ new Map();
3
21
  /** 查 sessionId 的 Skiff 角色名(无 → null)——向后兼容(guards/seams 依赖) */
@@ -22,4 +40,4 @@ function skiffSessionSnapshot() {
22
40
  return new Map(skiffSessions);
23
41
  }
24
42
  //#endregion
25
- export { unregisterSkiffSession as a, skiffSessionSnapshot as i, skiffRoleFor as n, skiffSessionInfo as r, registerSkiffSession as t };
43
+ export { unregisterSkiffSession as a, skiffSessionSnapshot as i, skiffRoleFor as n, ACC_VERSION as o, skiffSessionInfo as r, registerSkiffSession as t };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { h as resolveInside, m as readUtf8, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-NlLr_sxy.js";
2
+ import { g as resolveInside, h as readUtf8, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-DG4Oc7I1.js";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  //#region src/skiff-role.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-Bd_SjEs7.js";
1
+ import { r as cccRootForCwd } from "./ccc-roots-FxWXuZPw.js";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
4
4
  //#region src/time.ts
@@ -55,17 +55,34 @@ export declare function parseDeclaredSkills(sessionMd: string): string[];
55
55
  */
56
56
  export declare function hasTrajectorySkillsDeclaration(root: string, mdPath: string): boolean;
57
57
  /**
58
- * 装配注入正文(规格 §3):按声明顺序拼接各 skill 的 `SKILL.md` 全文,每段带来源抬头
58
+ * 合并两条声明来源(v1.44.0):**CCC 级在前,轨迹级追加**,同名去重取首次位置。
59
+ *
60
+ * 两条来源(owner 令 2026-09-20):
61
+ * · **CCC 级** = `.opencode/serenity.json` 的 `trajectory.skills`(本 CCC 所有轨迹共有的底座);
62
+ * · **轨迹级** = 该轨迹 `SESSION.md` frontmatter 的 `skills:`(这条轨迹额外的)。
63
+ *
64
+ * 为什么是**并集**而不是二选一(R↓):C1 设计当初否决"纯 CCC 全局"的理由是"会**丢掉
65
+ * per-trajectory**";并集把两侧都保住——容器给底座,单条轨迹仍可加自己的额外项。
66
+ * 为什么 CCC 级**在前**:底座先立、增量随后,与"入口身份块 order=-50 → 本 section order=-45"
67
+ * 的既有先后观感一致。
68
+ */
69
+ export declare function mergeSkillNames(cccSkills: readonly string[], trajSkills: readonly string[]): string[];
70
+ /**
71
+ * 装配注入正文(规格 §3):按**合并后的**顺序拼接各 skill 的 `SKILL.md` 全文,每段带来源抬头
59
72
  * `=== skill: <name> ===`;拿不到全文的一律以 `[缺失]` 响亮提示占位(**不静默**)。
60
73
  *
61
- * 安全(规格 §3 🔒):名字来自 CCC 数据(frontmatter)⇒ **先过 `isSafeSkillName`**;
74
+ * 安全(规格 §3 🔒):名字来自 CCC 数据(frontmatter / CCC 配置)⇒ **先过 `isSafeSkillName`**;
62
75
  * 不安全 ⇒ 按缺失处理,且不调用 `findSkillMd` ⇒ **不发生任何以该名拼出的 fs 访问**。
63
76
  *
64
- * 长度守卫:复用已导出的 `truncateContent`,超限截断并在末尾写明 `truncated`。
77
+ * 长度守卫:复用已导出的 `truncateContent`,**在合并之后**统一截断(两条来源共享同一上限,
78
+ * 不是各给 32 KB——否则上限会随来源数翻倍)。
65
79
  *
66
80
  * @param root CCC 根
67
81
  * @param mdPath 轨迹的 SESSION.md 路径(**来自绑定的 `mdPath`**,不用 label 拼)
68
82
  * @param maxChars 总长上限(字符)
69
- * @returns 注入正文;无声明 `''`(空串 = 宿主不产出该 section 块);SESSION.md 缺失 → 响亮提示
83
+ * @param cccSkills CCC 级声明(`readTrajectorySkills`;缺省空 = 只有轨迹级,行为与 v1.43 一致)
84
+ * @returns 注入正文;两条来源皆无声明 → `''`(空串 = 宿主不产出该 section 块);
85
+ * SESSION.md 缺失/读失败 → 响亮提示(若同时有 CCC 级声明,则提示在前、正文随后——
86
+ * **提示不因"别处有内容"而被吞掉**)
70
87
  */
71
- export declare function buildTrajectorySkillsSection(root: string, mdPath: string, maxChars?: number): string;
88
+ export declare function buildTrajectorySkillsSection(root: string, mdPath: string, maxChars?: number, cccSkills?: readonly string[]): string;
@@ -1,7 +1,7 @@
1
1
  import { r as hostService, t as hostAgents } from "./access-fiehjxV6.js";
2
- import { h as resolveInside } from "./ccc-NlLr_sxy.js";
3
- import { o as listCccs } from "./ccc-roots-Bd_SjEs7.js";
4
- import { D as localHuman, O as localIdStamp, T as isoLocal, b as sessionsRoot, n as listBoundSessionIds, p as findSession } from "./trajectory-bound-kxvMuCui.js";
2
+ import { g as resolveInside } from "./ccc-DG4Oc7I1.js";
3
+ import { o as listCccs } from "./ccc-roots-FxWXuZPw.js";
4
+ import { D as localHuman, O as localIdStamp, T as isoLocal, b as sessionsRoot, n as listBoundSessionIds, p as findSession } from "./trajectory-bound-BzdzsDPI.js";
5
5
  import { t as readSimpleSettings } from "./settings-section-DMDUoQum.js";
6
6
  import { basename, dirname, join } from "node:path";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
@@ -44,7 +44,7 @@ function emptyRegistry() {
44
44
  };
45
45
  }
46
46
  /** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整表) */
47
- function asEntry(value) {
47
+ function asEntry$1(value) {
48
48
  const e = value;
49
49
  if (!e || typeof e.id !== "string" || typeof e.target !== "string") return null;
50
50
  if (typeof e.at !== "string" || typeof e.message !== "string") return null;
@@ -78,7 +78,7 @@ function loadWakeRegistry(root) {
78
78
  const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
79
79
  const entries = [];
80
80
  for (const item of raw) {
81
- const e = asEntry(item);
81
+ const e = asEntry$1(item);
82
82
  if (e) entries.push(e);
83
83
  }
84
84
  return {
@@ -959,6 +959,210 @@ function registerCroTurnTracking(ctx) {
959
959
  });
960
960
  }
961
961
  //#endregion
962
+ //#region src/cro-log.ts
963
+ /**
964
+ * cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
965
+ *
966
+ * ## 所有者令(本模块的存在理由)
967
+ *
968
+ * 「S151 和 S185 的验证都说明,trajectory 倾向于自己做个结构化记录来记录自身 CRO 的执行情况,
969
+ * 这是个结构化流水账;**我们 ACC 有必要进行 CRO 唤醒的日志记录**,当 CRO 存在时,每次**成功唤醒**
970
+ * 写相关信息,但是保留数量要少,**只保留仅两日**的记录即可,**这个时间是程序化的**」
971
+ * +「**这个日志写结构化文件到 trajectory 目录,文件名固定即可**」
972
+ * +「**失败成功都记录**」。
973
+ *
974
+ * ## 它解决什么麻烦(白话)
975
+ *
976
+ * 在此之前「ACC 到底叫过谁、叫了几次、哪次**没叫成**」**ACC 自己一个字都不留** ——
977
+ * 要查只能去读**被叫的那个程序**写的记录;程序没写 / 写坏 / 被换掉 ⇒ **这段历史就是空白**。
978
+ * ⇒ 本模块让 ACC **自己那一侧**留一份只记两天的流水,使「唤起历史」**不再依赖被观测者是否老实**。
979
+ *
980
+ * ## 🔴 为什么**不是**第二真相源(本容器铁律,勿删此论证)
981
+ *
982
+ * 轨迹目录里因此**并排两个文件、主语各一**:
983
+ *
984
+ * | 文件 | 谁写 | 主语 | 回答 |
985
+ * |---|---|---|---|
986
+ * | `<轨迹目录>/cro-state.json`(CCC 侧程序自建,名字自定) | **CCC 的 CRO 程序** | 程序 | 「**我判了什么**」(含"没叫"的判定、心跳阈值、环境谓词) |
987
+ * | `<轨迹目录>/cro-wake-log.json`(**本模块**) | **ACC** | ACC | 「**我把什么送出去了、送成没有**」 |
988
+ *
989
+ * **内容不重叠**:本模块**不复制**判定细节(不存 `lastDecision` / 阈值 / 谓词);
990
+ * 而程序**不可能**知道投递结果(CRO 投递是 fire-and-forget)。
991
+ * ⇒ 两者**互补**:完整审计 = 两者合看(本模块 = 投递面|程序状态 = 判定面)。
992
+ *
993
+ * ## 🔴 轮转(所有者明示「程序化的」)
994
+ *
995
+ * **写入时按窗裁剪**:每次落盘只保留 `at >= now - CRO_LOG_RETENTION_MS` 的条目,
996
+ * 随后**原子写**(`tmp + rename`,与 `wake-registry.ts` 同款形态)。
997
+ * ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
998
+ * 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
999
+ *
1000
+ * ## 🔴 铁律(与 `cro.ts` 同族)
1001
+ *
1002
+ * **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
1003
+ * 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
1004
+ *
1005
+ * ## 保留量级(实测,非估计)
1006
+ *
1007
+ * 实测最高 = S185 的 40min 心跳(≈36 条/天);两天窗 ⇒ **≤ ~80 条、< 20 KB**。
1008
+ * 最坏(某程序每 5min 都叫)⇒ 288 条/天 ⇒ 两天 **≤ ~600 条、< 100 KB** —— 轮转**自带上限**。
1009
+ */
1010
+ /** 固定文件名(所有者令:「文件名固定即可」)——与程序自建的 `cro-state.json` 并排、主语分明 */
1011
+ const CRO_LOG_FILENAME = "cro-wake-log.json";
1012
+ /** 保留窗 = 2 日(所有者令「只保留仅两日」);**常量** ⇒ 「这个时间是程序化的」 */
1013
+ const CRO_LOG_RETENTION_MS = 1728e5;
1014
+ const CRO_LOG_VERSION = 1;
1015
+ /** 日志绝对路径 */
1016
+ function croWakeLogPath(root, dirName) {
1017
+ return join(sessionsRoot(root), dirName, CRO_LOG_FILENAME);
1018
+ }
1019
+ function emptyLog() {
1020
+ return {
1021
+ version: CRO_LOG_VERSION,
1022
+ entries: []
1023
+ };
1024
+ }
1025
+ /** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整档) */
1026
+ function asEntry(value) {
1027
+ const e = value;
1028
+ if (!e || typeof e !== "object") return null;
1029
+ if (typeof e.at !== "string" || e.at === "") return null;
1030
+ if (typeof e.ok !== "boolean") return null;
1031
+ if (typeof e.detail !== "string") return null;
1032
+ const digest = e.promptDigest ?? {};
1033
+ return {
1034
+ at: e.at,
1035
+ ok: e.ok,
1036
+ detail: e.detail,
1037
+ tick: typeof e.tick === "number" ? e.tick : 0,
1038
+ reason: typeof e.reason === "string" ? e.reason : null,
1039
+ promptDigest: {
1040
+ length: typeof digest.length === "number" ? digest.length : 0,
1041
+ head: typeof digest.head === "string" ? digest.head : ""
1042
+ }
1043
+ };
1044
+ }
1045
+ /** 提示词摘要(长度 + 前 N 字;超长不截断标记——长度字段本身就是信号) */
1046
+ function summarizeCroPrompt(prompt) {
1047
+ const text = typeof prompt === "string" ? prompt : "";
1048
+ return {
1049
+ length: [...text].length,
1050
+ head: [...text].slice(0, 80).join("")
1051
+ };
1052
+ }
1053
+ /**
1054
+ * 读取日志(**永不抛**;读坏 ⇒ 当作空档 + 返回 error 文本)。
1055
+ * @param root CCC 根
1056
+ * @param dirName 轨迹目录名
1057
+ */
1058
+ function loadCroWakeLog(root, dirName) {
1059
+ const path = croWakeLogPath(root, dirName);
1060
+ if (!existsSync(path)) return {
1061
+ log: emptyLog(),
1062
+ error: null
1063
+ };
1064
+ try {
1065
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
1066
+ const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
1067
+ const entries = [];
1068
+ for (const item of raw) {
1069
+ const e = asEntry(item);
1070
+ if (e) entries.push(e);
1071
+ }
1072
+ return {
1073
+ log: {
1074
+ version: CRO_LOG_VERSION,
1075
+ entries
1076
+ },
1077
+ error: null
1078
+ };
1079
+ } catch (err) {
1080
+ return {
1081
+ log: emptyLog(),
1082
+ error: `CRO 流水解析失败(${String(err?.message ?? err)})`
1083
+ };
1084
+ }
1085
+ }
1086
+ /**
1087
+ * 按窗裁剪(**纯函数** ⇒ 可穷举测试)。
1088
+ *
1089
+ * 判据:`Date.parse(at) >= nowMs - windowMs` 保留;反之丢弃。
1090
+ * 🔴 **`at` 不可解析者一律丢弃**(本模块只写 `isoLocal()` 产物 ⇒ 不可解析 = 被手改或损坏),
1091
+ * **并在返回值里报数**(`dropped`)——**不静默吞掉**(本容器反复栽的"安静失败"纪律)。
1092
+ */
1093
+ function pruneCroLogEntries(entries, nowMs, windowMs = CRO_LOG_RETENTION_MS) {
1094
+ const floor = nowMs - windowMs;
1095
+ const kept = [];
1096
+ let pruned = 0;
1097
+ let dropped = 0;
1098
+ for (const e of entries) {
1099
+ const t = Date.parse(e.at);
1100
+ if (Number.isNaN(t)) {
1101
+ dropped += 1;
1102
+ continue;
1103
+ }
1104
+ if (t >= floor) kept.push(e);
1105
+ else pruned += 1;
1106
+ }
1107
+ return {
1108
+ kept,
1109
+ pruned,
1110
+ dropped
1111
+ };
1112
+ }
1113
+ /**
1114
+ * 追加一条唤起记录并按窗裁剪(**本模块主入口;永不抛**)。
1115
+ *
1116
+ * 顺序 = 读 → 追加 → 裁剪 → **原子写**。任一步失败 ⇒ `{ok:false, error}`,
1117
+ * **不抛**(调用方只记一行 tick 日志;**绝不影响投递**)。
1118
+ *
1119
+ * 无 CRO 的轨迹**不会**因本函数产生文件:只有真的发生过一次唤起尝试才会建/写该档
1120
+ * (所有者令「**当 CRO 存在时**」)。
1121
+ *
1122
+ * @param root CCC 根
1123
+ * @param dirName 轨迹目录名
1124
+ * @param input 本次唤起尝试(成败都记)
1125
+ * @param nowMs 当前毫秒(可注入 ⇒ 便于测裁剪边界)
1126
+ * @returns 写入结果与裁剪计数
1127
+ */
1128
+ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
1129
+ const path = croWakeLogPath(root, dirName);
1130
+ try {
1131
+ const loaded = loadCroWakeLog(root, dirName);
1132
+ const entry = {
1133
+ at: isoLocal(nowMs),
1134
+ ok: input.ok,
1135
+ detail: input.detail,
1136
+ tick: input.tick,
1137
+ reason: input.reason,
1138
+ promptDigest: summarizeCroPrompt(input.prompt)
1139
+ };
1140
+ const { kept, pruned, dropped } = pruneCroLogEntries([...loaded.log.entries, entry], nowMs);
1141
+ mkdirSync(dirname(path), { recursive: true });
1142
+ const tmp = `${path}.tmp`;
1143
+ writeFileSync(tmp, `${JSON.stringify({
1144
+ version: CRO_LOG_VERSION,
1145
+ entries: kept
1146
+ }, null, 2)}\n`, "utf-8");
1147
+ renameSync(tmp, path);
1148
+ return {
1149
+ ok: true,
1150
+ error: null,
1151
+ kept: kept.length,
1152
+ pruned,
1153
+ dropped
1154
+ };
1155
+ } catch (err) {
1156
+ return {
1157
+ ok: false,
1158
+ error: `CRO 流水写入失败(${String(err?.message ?? err)})`,
1159
+ kept: 0,
1160
+ pruned: 0,
1161
+ dropped: 0
1162
+ };
1163
+ }
1164
+ }
1165
+ //#endregion
962
1166
  //#region src/clock-runtime.ts
963
1167
  /**
964
1168
  * clock-runtime.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
@@ -1594,6 +1798,16 @@ async function runCroPhase(ctx, root, registry, log) {
1594
1798
  wakes += 1;
1595
1799
  const res = await deliverCroWake(ctx, root, dirName, outcome.prompt, outcome.decision.reason);
1596
1800
  log.push(`· ${root}: ${renderCroOutcome(dirName, outcome)} → ${res.ok ? "已投递" : `投递未成功:${res.detail}`}`);
1801
+ if (!res.notReady) {
1802
+ const rec = appendCroWakeLog(root, dirName, {
1803
+ ok: res.ok,
1804
+ detail: res.detail,
1805
+ tick: s.ticks,
1806
+ reason: outcome.decision.reason ?? null,
1807
+ prompt: outcome.prompt
1808
+ }, nowMs);
1809
+ if (!rec.ok) log.push(`· ${root}: ${dirName}: CRO 流水写入失败(已忽略,不影响投递): ${rec.error}`);
1810
+ }
1597
1811
  } else if (outcome.status === "skipped") {
1598
1812
  skips += 1;
1599
1813
  log.push(`· ${root}: ${renderCroOutcome(dirName, outcome)}`);
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-NlLr_sxy.js";
2
+ import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-DG4Oc7I1.js";
3
3
  import { join, resolve } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { createHash } from "node:crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.43.0",
3
+ "version": "1.44.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2。",
5
5
  "license": "MIT",
6
6
  "repository": {