@shgroup/dsh-serenity-hooks 1.74.0 → 1.76.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/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { i as __toESM, n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
2
- import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-B2t_aM8L.js";
2
+ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-rwgzt_0F.js";
3
3
  import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
4
- import { A as sessionsRoot, C as namingTitleFor, D as readTrajectoryUid, E as readActiveSessionMd, F as isoLocal, I as localFileStamp, L as localHuman, M as showSession, N as summarize, O as resolveSessionByTitle, P as useSession, R as localIdStamp, S as listSessions, T as parseTrajectoryCode, _ as extractSessionMdPathFromText, a as readTrajectoryConstraintAt, c as registerTrajectorySession$1, d as COGNITIVE_TRAJECTORY_ACTIONS, f as DEFAULT_SESSION_SCOPE, g as createSession, h as clearActiveSessionInfo, j as setActiveSessionInfo, k as sessionEvents$1, l as trajectoryDirNameForSession, m as archiveSession, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, p as UID_FILENAME, r as listTrajectoryConstraints, s as trajectoryConstraintForSession, t as TRAJECTORY_CONSTRAINT_FILENAME, u as unregisterTrajectorySession$1, v as findSession, w as parseSessionContextFromEvents, x as listActiveTrajectories, y as getActiveSessionInfo } from "./profile-BVt84mbi.js";
4
+ import { A as sessionsRoot, B as localFileStamp, C as namingTitleFor, D as readTrajectoryUid, E as readActiveSessionMd, F as useSession, H as localIdStamp, I as TRAJECTORY_FILENAME, L as TRAJECTORY_LEGACY_FILENAMES, M as showSession, N as summarize, O as resolveSessionByTitle, P as trajectoryDescOf, R as readTrajectoryFile, S as listSessions, T as parseTrajectoryCode, V as localHuman, _ as extractSessionMdPathFromText, a as readTrajectoryConstraintAt, c as registerTrajectorySession$1, d as COGNITIVE_TRAJECTORY_ACTIONS, f as DEFAULT_SESSION_SCOPE, g as createSession, h as clearActiveSessionInfo, j as setActiveSessionInfo, k as sessionEvents$1, l as trajectoryDirNameForSession, m as archiveSession, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, p as UID_FILENAME, r as listTrajectoryConstraints, s as trajectoryConstraintForSession, t as TRAJECTORY_CONSTRAINT_FILENAME, u as unregisterTrajectorySession$1, v as findSession, w as parseSessionContextFromEvents, x as listActiveTrajectories, y as getActiveSessionInfo, z as isoLocal } from "./profile-DplLGGqo.js";
5
5
  import { _ as runLocalStore, a as readFeishuCredential, d as weixinSessionIdForTrajectory, f as LOCALSTORE_SCOPES, g as readStore, h as readGitTrack, i as matchWeixinRoute, l as sanitizeFileName$1, m as localstorePath, n as extractWeixinText, o as readWeixinCredential, p as checkLocalstoreGitCompliance, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-COOFLTpU.js";
6
- import { a as buildTrajectoryBasePrompt, c as createProfilePromptReader, d as resolveConstraintSystemPrompt, f as ACC_VERSION, i as TRAJECTORY_CONSTRAINT_HINT, l as profileMsmWhitelist, n as readSimpleSettings, o as constraintCognitiveTrajectory, p as findPackageRoot, r as registerSettingsSection, s as constraintSystemPromptSource, u as profileToolWhitelist } from "./settings-D8hlqxua.js";
7
- import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-DCpoRHP3.js";
6
+ import { a as buildTrajectoryBasePrompt, c as createProfilePromptReader, d as resolveConstraintSystemPrompt, f as ACC_VERSION, i as TRAJECTORY_CONSTRAINT_HINT, l as profileMsmWhitelist, n as readSimpleSettings, o as constraintCognitiveTrajectory, p as findPackageRoot, r as registerSettingsSection, s as constraintSystemPromptSource, u as profileToolWhitelist } from "./settings-f9WZXcgi.js";
7
+ import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-C_Zd2vJh.js";
8
8
  import { REGISTERED_TOOLS } from "./invariant.js";
9
9
  import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
10
10
  import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
@@ -994,6 +994,68 @@ function weixinSendEndpoint() {
994
994
  return endpoint;
995
995
  }
996
996
  //#endregion
997
+ //#region src/tools/config-docs.ts
998
+ /**
999
+ * 渲染**目录**(第一层)。
1000
+ *
1001
+ * 🔴 只调 `summary`,**绝不调 `render`** —— 目录若触发正文,就退化成"整份长文"。
1002
+ * 这条不是性能优化,是**判据**:它保证 `docs` 的输出大小与章数成正比,与正文长度无关。
1003
+ */
1004
+ function renderDocsIndex(chapters) {
1005
+ const lines = [
1006
+ "═══ CCC 配置说明 · 目录 ═══",
1007
+ "",
1008
+ "说明类内容已超过单次返回的合适篇幅 ⇒ 先读目录,再按章取详情。",
1009
+ "",
1010
+ "用法:container_admin config docs <章节>",
1011
+ ""
1012
+ ];
1013
+ for (const ch of chapters) lines.push(` ${ch.id.padEnd(14)}${ch.summary}`);
1014
+ lines.push("");
1015
+ lines.push("旧入口(msm catalog / msm ccc-config / config guide)**降为指针**,只指路、不再返回正文。");
1016
+ return lines.join("\n");
1017
+ }
1018
+ /**
1019
+ * 解析一个章节参数(第二层)。
1020
+ *
1021
+ * 不存在 ⇒ 返回 `null`,由调用方给出**带候选清单**的拒绝 ——
1022
+ * "不知道这个章节" 与 "这一章是空的" 必须可区分(否则用户以为功能没有)。
1023
+ */
1024
+ function findDocsChapter(chapters, id) {
1025
+ const want = id.trim().toLowerCase();
1026
+ if (want === "") return null;
1027
+ return chapters.find((c) => c.id.toLowerCase() === want) ?? null;
1028
+ }
1029
+ /**
1030
+ * **指针文案**(v1.75.0,owner 裁 Q3「指针」)——被降级的旧入口共用这一份。
1031
+ *
1032
+ * ## 为什么要有"指针"这个形态(而不是直接删掉旧入口)
1033
+ *
1034
+ * 旧入口的名字(`msm catalog` / `msm ccc-config` / `config guide`)**已经写在 CCC 侧的
1035
+ * 文档与提示词里**,硬删会让它们全部变成"未知动作",而且**没有任何机械信号**告诉我们
1036
+ * 哪份文档已经说谎了(本仓 D151 同族)。
1037
+ *
1038
+ * ⇒ 保留名字、只换返回内容:调用方能**自己顺着指针找到正文**,而正文仍然只有一份。
1039
+ *
1040
+ * 🔴 **必须只有一处实现**:`container-admin.ts`(config 域)与 `msm/service.ts`(msm 域)
1041
+ * 都要用它。两处各写一份文案 ⇒ 措辞漂移(本仓反复踩过的形态)。
1042
+ *
1043
+ * @param chapter 正文所在的章 id
1044
+ * @param what 这个入口原来叫什么(进标题,让调用方一眼确认自己找对了)
1045
+ */
1046
+ function docsPointer(chapter, what) {
1047
+ return [
1048
+ `═══ ${what} — 正文已统一 ═══`,
1049
+ "",
1050
+ `正文在:container_admin config docs ${chapter}`,
1051
+ "",
1052
+ "为什么改(v1.75.0,owner 裁「指针」):这个内容此前有**两个**正文入口,",
1053
+ "而它们返回同一份常量 ⇒ 两个真相源。现在「一个内容一个正文入口」,本入口只指路。",
1054
+ "",
1055
+ "索引(不知道还有哪些章):container_admin config docs"
1056
+ ].join("\n");
1057
+ }
1058
+ //#endregion
997
1059
  //#region src/tools/msm/service.ts
998
1060
  /**
999
1061
  * tools/msm/service.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
@@ -1069,6 +1131,10 @@ const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx";
1069
1131
  * 目录在前、详情各归各——**单一真相源**:本目录只索引"去哪个工具/子命令看详情",不复制详情
1070
1132
  * (避免 container_admin/weixin-doctor 等 guide 全文重复 → 多真相源 → 失同步)。
1071
1133
  * 后续新增功能只需在此加一行(低熵可演进)。
1134
+ *
1135
+ * 🔴 **2026-10-19 v1.75.0 起导出**:它是 `container_admin config docs catalog` 那一章的正文。
1136
+ * `msm catalog` 动作**降为指针**(只指路,不再返回本常量)—— 否则同一份正文会有两个入口,
1137
+ * 而"两个入口返回同一份内容"正是本目录自己反对的形态。
1072
1138
  */
1073
1139
  const ACC_CATALOG = `═══ ACC Usage Catalog ═══
1074
1140
  Directory of ACC capabilities — each area lists where to go for details (guides live with their feature; this catalog only points).
@@ -1552,7 +1618,7 @@ function runMsm(root, args) {
1552
1618
  return `${header}\n` + lines.join("\n");
1553
1619
  }
1554
1620
  case "guide": return { guide: MSM_GUIDE };
1555
- case "catalog": return { catalog: ACC_CATALOG };
1621
+ case "catalog": return { catalog: docsPointer("catalog", "ACC 能力目录") };
1556
1622
  case "exec": {
1557
1623
  const { entry, businessArgs, fmtJson, hasHelp, protocol } = prepareExec(root, args);
1558
1624
  const p = protocolResult(protocol);
@@ -1676,7 +1742,7 @@ function runMsm(root, args) {
1676
1742
  }
1677
1743
  throw new Error(`MSM not registered: "${name}"`);
1678
1744
  }
1679
- case "ccc-config": return CCC_CONFIG_REFERENCE;
1745
+ case "ccc-config": return docsPointer("config", "CCC 配置参考");
1680
1746
  case "check": return checkRegistryQuality(root);
1681
1747
  default: throw new Error(`Unknown action: ${args.action}`);
1682
1748
  }
@@ -4079,7 +4145,7 @@ const TRAJECTORY_PRESET = "standard";
4079
4145
  * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现"轨迹 ↔ 会话"长期映射)
4080
4146
  */
4081
4147
  async function createTrajectoryAgent(ctx, root, profile, defaultModel, sessionId) {
4082
- if (!ctx.agents) throw new Error("轨迹约束: ctx.agents unavailable");
4148
+ if (!ctx.agents) throw new Error("trajectory constraint: ctx.agents unavailable");
4083
4149
  const model = profile.model?.trim() || defaultModel || "";
4084
4150
  const id = sessionId ?? `轨迹约束-${profile.code}-${randomUUID()}`;
4085
4151
  const setup = async (agentCtx) => {
@@ -4306,7 +4372,7 @@ function trajectoryConstraintMsmGate(root, sessionId, action, name) {
4306
4372
  if (sessionId === void 0) return {};
4307
4373
  const profile = trajectoryConstraintFor(root, sessionId);
4308
4374
  if (profile === null) return {};
4309
- if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in 轨迹约束 sessions" };
4375
+ if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in trajectory-constraint sessions" };
4310
4376
  if (action === "exec") {
4311
4377
  if (!name || !profile.msms.includes(name)) return { reject: "MSM not allowed" };
4312
4378
  return {};
@@ -10094,6 +10160,110 @@ function listWakes(root, opts = {}) {
10094
10160
  };
10095
10161
  }
10096
10162
  //#endregion
10163
+ //#region src/trajectory/user-carrier.ts
10164
+ /**
10165
+ * trajectory/user-carrier.ts — **造一条与"用户人工创建"基本一致的 dsh 会话载体**(判决 D171,owner 2026-10-20 裁 B)
10166
+ *
10167
+ * ## 这个文件解决什么(白话)
10168
+ *
10169
+ * `send-now` 在目标没有活跃会话时会**就地建一条**。它一直能建、也能挂工作区,
10170
+ * 但建出来的东西与用户在 UI 里新建的那条**在宿主眼里不是同一种东西**:
10171
+ *
10172
+ * | 宿主投影 | 用户建 | 我们自建(修前实测) |
10173
+ * |---|---|---|
10174
+ * | `title` | 自动生成 | **永远 `null`** |
10175
+ * | `titleInput.count` | ≥1 | **0** |
10176
+ * | 假首轮(对齐声明 + EAP) | 注入 | **被自家递归守卫挡掉** |
10177
+ *
10178
+ * ## 🔴 根因(读宿主源码,不是推断)
10179
+ *
10180
+ * `dsh-session-title` 的 `sessionTitleUserMessageOf()` **第一行**就是:
10181
+ *
10182
+ * ```js
10183
+ * if (event.type !== "user/message" || event.data.source.kind !== "user") return void 0
10184
+ * ```
10185
+ *
10186
+ * 而 ACC 投递的消息一律是 `{ kind: 'plugin:dsh-serenity-hooks' }`(`kernel/message-source.ts`)
10187
+ * ⇒ **宿主从头到尾不把它当"有真人说过话"的会话** ⇒ 自动命名不触发、`titleInput` 恒 0。
10188
+ *
10189
+ * ## owner 的裁决 = B:**模拟一个用户开场白**
10190
+ *
10191
+ * `MessageSourceMap` 里 `user: { kind: 'user' }`,而 `UserMessage.source` 是**任意**
10192
+ * `MessageSource` ⇒ `createUserMessage({ content, source: { kind: 'user' } })` **类型合法、
10193
+ * 零改 DSH**(D4 仍成立)。
10194
+ *
10195
+ * ## 🔴 这是一次**明知故犯的伪造**,代价写在这里,不许悄悄付
10196
+ *
10197
+ * 1. **伪造用户身份**:与 D160「不再造历史」方向相反。范围**仅限**"载体开场白"这一条消息,
10198
+ * 且正文**逐字自证**是模拟的({@link CARRIER_OPENING_MARK})⇒ 将来翻日志的人一眼能看出。
10199
+ * 2. **污染所有者语料**:`session-corpus` 的"所有者发言"判据**必须**排除这条
10200
+ * (判据 = 正文含 {@link CARRIER_OPENING_MARK})⇒ 否则统计"owner 说过什么"会算进一句
10201
+ * 机器写的开场白。**这一条是同批改动,不是待办。**
10202
+ * 3. **与假首轮的先后**:开场白必须让假首轮先注入(同一次 `agent/inbox/inserted` 里
10203
+ * prepend 天然排在真实消息之前),否则 `hasUserMessageHistory` 会把假首轮挡掉。
10204
+ * ⇒ 单测钉死顺序,不靠"应该会"。
10205
+ *
10206
+ * ## 归属(D23)
10207
+ * 机制在 ACC(怎么建、建什么形状);**内容归 CCC**(开场白正文由调用方给)。
10208
+ *
10209
+ * ## 与 `trajectory/carrier.ts` 的关系
10210
+ * 🔴 **两个文件、两件事**,名字刻意区分:`carrier.ts` 回答"**这条载体跑完没有**"(等待空闲/销毁竞速);
10211
+ * 本文件回答"**怎么造一条载体**"。合在一起会让两条互不相关的依赖链卷成一团。
10212
+ */
10213
+ /**
10214
+ * 模拟开场白的**自证标记**(逐字)。
10215
+ *
10216
+ * 🔴 三个用途,缺一不可:
10217
+ * ① 人类翻日志时能一眼看出"这不是真主说的";
10218
+ * ② `session-corpus` 用它**排除**语料(不污染"所有者发言");
10219
+ * ③ 单测反向钉住:真实用户发言里**不会**出现这串 ⇒ 断言"语料里 0 条含它"才有意义。
10220
+ */
10221
+ const CARRIER_OPENING_MARK = "[ACC 代用户开场]";
10222
+ /**
10223
+ * 构造模拟开场白的正文。
10224
+ *
10225
+ * ⚠️ **不含版本号/日期/模型名**(D159:任务书里不得写死版本号 ⇒ 写与版本无关的判据)。
10226
+ * 正文只描述"这是谁、要接着做什么",不描述"哪一版干的"。
10227
+ *
10228
+ * @param code 轨迹编码(`S###` 或自定义)
10229
+ * @param desc 轨迹目录名里那段可读描述(**人起的**,不另造第二真相源)
10230
+ * @returns 一条自证是模拟的开场白正文
10231
+ */
10232
+ function buildCarrierOpening(code, desc) {
10233
+ const label = desc.trim() === "" ? code : `${code}(${desc})`;
10234
+ return [
10235
+ `${CARRIER_OPENING_MARK} 激活认知轨迹 ${label},接着它 SESSION.md 里的工作往下做。`,
10236
+ "",
10237
+ "(这条开场白由 ACC 代为发出。如此做的目的只有一个:让程序性创建的会话与用户在界面里",
10238
+ "人工创建的会话,在宿主侧具有相同的投影 —— 有标题、有首轮对齐声明。它不是任何人的发言。)"
10239
+ ].join("\n");
10240
+ }
10241
+ /**
10242
+ * 投一条"用户形态"的消息进目标 agent(**B 的核心动作**)。
10243
+ *
10244
+ * 🔴 为什么单独成函数而不是就地写:这是**全仓唯一**允许 `kind:'user'` 的地方。
10245
+ * 集中在一处,将来若要收紧(或 owner 改主意回 A),只有一个点要改。
10246
+ *
10247
+ * @param agent 目标 agent(须有 `followup`)
10248
+ * @param text 开场白正文(**必须**由 {@link buildCarrierOpening} 产出,以带上自证标记)
10249
+ * @returns 投递是否成功(失败返回 false,由调用方决定降级 —— 不抛)
10250
+ */
10251
+ function deliverCarrierOpening(agent, text) {
10252
+ if (typeof agent.followup !== "function") return false;
10253
+ try {
10254
+ agent.followup(createUserMessage({
10255
+ content: [{
10256
+ type: "text",
10257
+ text
10258
+ }],
10259
+ source: { kind: "user" }
10260
+ }));
10261
+ return true;
10262
+ } catch {
10263
+ return false;
10264
+ }
10265
+ }
10266
+ //#endregion
10097
10267
  //#region src/cro/core.ts
10098
10268
  /**
10099
10269
  * cro/core.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
@@ -10331,6 +10501,16 @@ function croScriptPath(root, dirName) {
10331
10501
  * —— 程序还没跑,没有快照可读。⇒ 只能读文件。(这也是"机制属 ACC"的体现:
10332
10502
  * ACC 在调用用户程序前先判定自己该不该调用。)
10333
10503
  *
10504
+ * ## 🆕 2026-10-19 owner 裁 Q2:**声明文件里的 `cro.host` 优先,独立文件回落**
10505
+ *
10506
+ * 收拢后机器名住在 `<轨迹目录>/trajectory.json` 的 `cro.host` 字段里。
10507
+ * 判据(**两代并读,不硬切**):**声明面里有合法 `cro.host` ⇒ 用它;否则回落 `cro-host-binding` 文件**。
10508
+ *
10509
+ * 🔴 **为什么这里绝不能硬切**(比别处更硬的一条,逐字保留原裁定):
10510
+ * 硬切之后,**第二台机器**读到的是「没绑」而不是「绑给了别人」——
10511
+ * 两者都导致"不跑",**表面结果相同,但判据退化**(失去可诊断性)。
10512
+ * ⇒ 只要还有一台机器可能装着旧版本,两条路都必须能读。
10513
+ *
10334
10514
  * @param root CCC 根
10335
10515
  * @param dirName 轨迹目录名
10336
10516
  * @param selfHost 本机机器名(**注入** ⇒ 可测;生产调用方传 `hostname()`)
@@ -10350,6 +10530,8 @@ function resolveCroHostBinding(root, dirName, selfHost) {
10350
10530
  detail: `绑定文件路径非法(已忽略):${String(err?.message ?? err)}`
10351
10531
  };
10352
10532
  }
10533
+ const fromJson = readCroHostFromDeclaration(root, dirName);
10534
+ if (fromJson !== null) return verdictFor(fromJson, self);
10353
10535
  let raw;
10354
10536
  try {
10355
10537
  if (!existsSync(fileAbs)) return {
@@ -10357,7 +10539,7 @@ function resolveCroHostBinding(root, dirName, selfHost) {
10357
10539
  bound: null,
10358
10540
  self,
10359
10541
  code: "not-bound",
10360
- detail: `未绑定机器(缺 ${CRO_HOST_FILENAME})⇒ 按"必须指定"不运行`
10542
+ detail: `未绑定机器(缺 ${CRO_HOST_FILENAME},且 ${TRAJECTORY_FILENAME} 里也没有 cro.host)⇒ 按"必须指定"不运行`
10361
10543
  };
10362
10544
  const st = statSync(fileAbs);
10363
10545
  if (st.size > CRO_HOST_FILE_MAX) return {
@@ -10385,6 +10567,37 @@ function resolveCroHostBinding(root, dirName, selfHost) {
10385
10567
  code: "not-bound",
10386
10568
  detail: `绑定文件为空(只有注释/空行)⇒ 按"必须指定"不运行`
10387
10569
  };
10570
+ return verdictFor(bound, self);
10571
+ }
10572
+ /**
10573
+ * 从**声明文件**(`trajectory.json` 族)里取 `cro.host`(取不到 ⇒ `null`)。
10574
+ *
10575
+ * 🔴 **永不抛**:本函数跑在 tick 内(旁路容忍铁律);坏 JSON / 结构不符一律按"没给"处理,
10576
+ * 于是**自然回落到独立文件那条路** —— 这正是两代并读想要的降级方向。
10577
+ *
10578
+ * @param root CCC 根
10579
+ * @param dirName 轨迹目录名
10580
+ * @returns 机器名;没给 ⇒ `null`
10581
+ */
10582
+ function readCroHostFromDeclaration(root, dirName) {
10583
+ try {
10584
+ const cro = readTrajectoryFile(join(sessionsRoot(root), dirName))?.data?.["cro"];
10585
+ if (!cro || typeof cro !== "object" || Array.isArray(cro)) return null;
10586
+ const host = cro["host"];
10587
+ if (typeof host !== "string") return null;
10588
+ const trimmed = host.trim();
10589
+ return trimmed === "" ? null : trimmed;
10590
+ } catch {
10591
+ return null;
10592
+ }
10593
+ }
10594
+ /**
10595
+ * 已解析出的机器名 → 判定结果(三种态的唯一收敛点)。
10596
+ *
10597
+ * 🔴 **抽出来的理由**:两条读取路(声明文件 / 独立文件)必须给出**同一种**判定语义;
10598
+ * 各写一份迟早会漂("声明面绑给别人"与"文件面绑给别人"在日志里长得不一样就糟了)。
10599
+ */
10600
+ function verdictFor(bound, self) {
10388
10601
  if (bound === self) return {
10389
10602
  allowed: true,
10390
10603
  bound,
@@ -10921,12 +11134,36 @@ function registerCroTurnTracking(ctx) {
10921
11134
  * | 文件 | 谁写 | 主语 | 回答 |
10922
11135
  * |---|---|---|---|
10923
11136
  * | `<轨迹目录>/cro-state.json`(CCC 侧程序自建,名字自定) | **CCC 的 CRO 程序** | 程序 | 「**我判了什么**」(含"没叫"的判定、心跳阈值、环境谓词) |
10924
- * | `<轨迹目录>/cro-wake-log.json`(**本模块**) | **ACC** | ACC | 「**我把什么送出去了、送成没有**」 |
11137
+ * | `<轨迹目录>/cro-wake-log.log`(**本模块**) | **ACC** | ACC | 「**我把什么送出去了、送成没有**」 |
10925
11138
  *
10926
11139
  * **内容不重叠**:本模块**不复制**判定细节(不存 `lastDecision` / 阈值 / 谓词);
10927
11140
  * 而程序**不可能**知道投递结果(CRO 投递是 fire-and-forget)。
10928
11141
  * ⇒ 两者**互补**:完整审计 = 两者合看(本模块 = 投递面|程序状态 = 判定面)。
10929
11142
  *
11143
+ * ## 🔴 形态 = 纯文本行,不是 JSON(owner 2026-10-19 裁 Q1)
11144
+ *
11145
+ * owner 原话:「**日志不要写 json,不合适,日志直接写纯文本 `.log` 就行**」。
11146
+ * ⇒ 一行一条,**tab 分隔**(终端里按制表位对齐,`grep` / `cut` / `awk` 直接可用):
11147
+ *
11148
+ * ```text
11149
+ * # ACC CRO 唤起流水(纯文本 · tab 分隔 · 时间 / 结果 / tick / 方式 / 理由 / 提示词摘要)
11150
+ * 2026-10-19T22:15:03+08:00 ok tick=82 已投递(live) reason=心跳:距上次 44m prompt=17:请先整理你的 SESSION.md
11151
+ * ```
11152
+ *
11153
+ * 🔴 **`#` 开头是注释 / 表头行,解析时跳过** —— 纯文本档案缺了表头,下一个人(或半年后的我)
11154
+ * 打开只看到一堆制表符,那是把 R 推给读者。
11155
+ * 🔴 **坏行跳过而非报错**(与 JSON 版的 `asEntry()` 同口径):手改 / 半行 / 截断都不得让整档失效。
11156
+ *
11157
+ * ## 🔴 两代并读(**不硬切**,本容器反复栽过的形态)
11158
+ *
11159
+ * 旧名 `cro-wake-log.json` **仍然读**(存量轨迹里可能正有那份);
11160
+ * **只写新 `.log`**。读侧判据 = **「新档在就只读新档,否则回落旧档」**。
11161
+ *
11162
+ * 🔴 **首次写入 = 迁移,不是覆盖**:那一刻 `loadCroWakeLog` 读到的仍是旧档条目,
11163
+ * 它们与新条目一起过裁剪、一起落进新档 ⇒ **窗内的旧记录不会凭空消失**;
11164
+ * 而旧档此后不再被读(新档已在)⇒ **不会重复计入**,也不需要删除动作。
11165
+ * ⚠️ 这条**只能靠读源码知道**(写出来的档看不出它继承过谁)⇒ 故在此明写。
11166
+ *
10930
11167
  * ## 🔴 轮转(所有者明示「程序化的」)
10931
11168
  *
10932
11169
  * **写入时按窗裁剪**:每次落盘只保留 `at >= now - CRO_LOG_RETENTION_MS` 的条目,
@@ -10945,21 +11182,107 @@ function registerCroTurnTracking(ctx) {
10945
11182
  * 最坏(某程序每 5min 都叫)⇒ 288 条/天 ⇒ 两天 **≤ ~600 条、< 100 KB** —— 轮转**自带上限**。
10946
11183
  */
10947
11184
  /** 固定文件名(所有者令:「文件名固定即可」)——与程序自建的 `cro-state.json` 并排、主语分明 */
10948
- const CRO_LOG_FILENAME = "cro-wake-log.json";
11185
+ const CRO_LOG_FILENAME = "cro-wake-log.log";
11186
+ /**
11187
+ * 🔴 **读取族 = 新名在前、旧名在后**(认族,不认"当前叫什么")。
11188
+ *
11189
+ * 本容器已两次栽在"上游改名、下游写死旧名":`skiff.json` → `trajectory-constraint.json` 那次,
11190
+ * 症状是「写不进去」,而真因是「文件名对不上」,两者在读数上完全不像。
11191
+ * ⇒ 上游再改名只往族里加一项。
11192
+ */
11193
+ const CRO_LOG_LEGACY_FILENAMES = ["cro-wake-log.json"];
10949
11194
  /** 保留窗 = 2 日(所有者令「只保留仅两日」);**常量** ⇒ 「这个时间是程序化的」 */
10950
11195
  const CRO_LOG_RETENTION_MS = 1728e5;
10951
- const CRO_LOG_VERSION = 1;
10952
- /** 日志绝对路径 */
11196
+ /** 纯文本档案的表头行(解析时跳过;人打开时先看到它) */
11197
+ const CRO_LOG_HEADER = "# ACC CRO 唤起流水(纯文本 · tab 分隔 · 时间 / 结果 / tick / 方式 / 理由 / 提示词摘要)";
11198
+ /** 日志绝对路径(**写入目标** = 新名) */
10953
11199
  function croWakeLogPath(root, dirName) {
10954
11200
  return join(sessionsRoot(root), dirName, CRO_LOG_FILENAME);
10955
11201
  }
11202
+ /** 旧档绝对路径(**只读**,用于两代并读) */
11203
+ function croWakeLogLegacyPath(root, dirName, filename) {
11204
+ return join(sessionsRoot(root), dirName, filename);
11205
+ }
10956
11206
  function emptyLog() {
10957
11207
  return {
10958
- version: CRO_LOG_VERSION,
11208
+ version: 1,
10959
11209
  entries: []
10960
11210
  };
10961
11211
  }
10962
- /** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整档) */
11212
+ /**
11213
+ * 抹掉会破坏**行结构**的字符(tab / CR / LF)。
11214
+ *
11215
+ * 🔴 只抹这三个:它们是**分隔符**,混进来会让一行变成两行或错位;
11216
+ * 其余字符(`|`、`]`、中文标点)**原样保留** —— 那些是内容,抹了就是把 E 降了。
11217
+ */
11218
+ function oneline(text) {
11219
+ return text.replace(/[\t\r\n]+/g, " ");
11220
+ }
11221
+ /** 一条记录 → 纯文本行(tab 分隔、字段带前缀便于人读) */
11222
+ function formatCroLogLine(entry) {
11223
+ const reason = entry.reason === null ? "" : oneline(entry.reason);
11224
+ const head = oneline(entry.promptDigest.head);
11225
+ return [
11226
+ oneline(entry.at),
11227
+ entry.ok ? "ok" : "fail",
11228
+ `tick=${entry.tick}`,
11229
+ oneline(entry.detail),
11230
+ `reason=${reason}`,
11231
+ `prompt=${entry.promptDigest.length}:${head}`
11232
+ ].join(" ");
11233
+ }
11234
+ /**
11235
+ * 纯文本行 → 记录(**坏行返回 null 而不是抛**)。
11236
+ *
11237
+ * 判据:字段数不足 / 时间不可解析 / `ok` 标记不认识 ⇒ `null`(跳过该行,其余照读)。
11238
+ * 🔴 `at` 必须**可被 `Date.parse` 解析**才收 —— 那是裁剪的前提,
11239
+ * 收一条解析不了的时间等于让它在此档里永久驻留(轮转对它失效)。
11240
+ */
11241
+ function parseCroLogLine(line) {
11242
+ if (line === "" || line.startsWith("#")) return null;
11243
+ const parts = line.split(" ");
11244
+ if (parts.length < 6) return null;
11245
+ const at = parts[0];
11246
+ if (Number.isNaN(Date.parse(at))) return null;
11247
+ const mark = parts[1];
11248
+ if (mark !== "ok" && mark !== "fail") return null;
11249
+ const tickM = /^tick=(\d+)$/.exec(parts[2]);
11250
+ if (!tickM) return null;
11251
+ const reasonRaw = parts[4].startsWith("reason=") ? parts[4].slice(7) : "";
11252
+ const promptM = /^prompt=(\d+):([\s\S]*)$/.exec(parts[5]);
11253
+ return {
11254
+ at,
11255
+ ok: mark === "ok",
11256
+ detail: parts[3],
11257
+ tick: Number(tickM[1]),
11258
+ reason: reasonRaw === "" ? null : reasonRaw,
11259
+ promptDigest: {
11260
+ length: promptM ? Number(promptM[1]) : 0,
11261
+ head: promptM ? promptM[2] : ""
11262
+ }
11263
+ };
11264
+ }
11265
+ /** 全文 → 记录(跳过表头与坏行;**永不抛**) */
11266
+ function parseCroLogText(text) {
11267
+ const entries = [];
11268
+ for (const line of text.split("\n")) {
11269
+ const e = parseCroLogLine(line);
11270
+ if (e) entries.push(e);
11271
+ }
11272
+ return entries;
11273
+ }
11274
+ /** JSON 旧档 → 记录(字段缺失/类型不符 ⇒ 丢弃该条,不阻断整档) */
11275
+ function parseCroLogJson(text) {
11276
+ const parsed = JSON.parse(text);
11277
+ const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
11278
+ const entries = [];
11279
+ for (const item of raw) {
11280
+ const e = asEntry(item);
11281
+ if (e) entries.push(e);
11282
+ }
11283
+ return entries;
11284
+ }
11285
+ /** 旧档条目形状校验(容忍手改) */
10963
11286
  function asEntry(value) {
10964
11287
  const e = value;
10965
11288
  if (!e || typeof e !== "object") return null;
@@ -10989,27 +11312,20 @@ function summarizeCroPrompt(prompt) {
10989
11312
  }
10990
11313
  /**
10991
11314
  * 读取日志(**永不抛**;读坏 ⇒ 当作空档 + 返回 error 文本)。
11315
+ *
11316
+ * 🔴 **两代并读的判据**:新档在 ⇒ **只读新档**;否则按族回落旧档。
11317
+ * 这样首次写入之后旧档自然退场,**不需要删除动作、也不会重复计入**。
11318
+ *
10992
11319
  * @param root CCC 根
10993
11320
  * @param dirName 轨迹目录名
10994
11321
  */
10995
11322
  function loadCroWakeLog(root, dirName) {
10996
11323
  const path = croWakeLogPath(root, dirName);
10997
- if (!existsSync(path)) return {
10998
- log: emptyLog(),
10999
- error: null
11000
- };
11001
- try {
11002
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
11003
- const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
11004
- const entries = [];
11005
- for (const item of raw) {
11006
- const e = asEntry(item);
11007
- if (e) entries.push(e);
11008
- }
11324
+ if (existsSync(path)) try {
11009
11325
  return {
11010
11326
  log: {
11011
- version: CRO_LOG_VERSION,
11012
- entries
11327
+ version: 1,
11328
+ entries: parseCroLogText(readFileSync(path, "utf-8"))
11013
11329
  },
11014
11330
  error: null
11015
11331
  };
@@ -11019,6 +11335,28 @@ function loadCroWakeLog(root, dirName) {
11019
11335
  error: `CRO 流水解析失败(${String(err?.message ?? err)})`
11020
11336
  };
11021
11337
  }
11338
+ for (const legacy of CRO_LOG_LEGACY_FILENAMES) {
11339
+ const legacyPath = croWakeLogLegacyPath(root, dirName, legacy);
11340
+ if (!existsSync(legacyPath)) continue;
11341
+ try {
11342
+ return {
11343
+ log: {
11344
+ version: 1,
11345
+ entries: parseCroLogJson(readFileSync(legacyPath, "utf-8"))
11346
+ },
11347
+ error: null
11348
+ };
11349
+ } catch (err) {
11350
+ return {
11351
+ log: emptyLog(),
11352
+ error: `CRO 流水解析失败(${String(err?.message ?? err)})`
11353
+ };
11354
+ }
11355
+ }
11356
+ return {
11357
+ log: emptyLog(),
11358
+ error: null
11359
+ };
11022
11360
  }
11023
11361
  /**
11024
11362
  * 按窗裁剪(**纯函数** ⇒ 可穷举测试)。
@@ -11047,6 +11385,10 @@ function pruneCroLogEntries(entries, nowMs, windowMs = CRO_LOG_RETENTION_MS) {
11047
11385
  dropped
11048
11386
  };
11049
11387
  }
11388
+ /** 记录集 → 落盘全文(表头 + 逐行 + 末尾换行) */
11389
+ function renderCroLogText(entries) {
11390
+ return `${[CRO_LOG_HEADER, ...entries.map(formatCroLogLine)].join("\n")}\n`;
11391
+ }
11050
11392
  /**
11051
11393
  * 追加一条唤起记录并按窗裁剪(**本模块主入口;永不抛**)。
11052
11394
  *
@@ -11077,10 +11419,7 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
11077
11419
  const { kept, pruned, dropped } = pruneCroLogEntries([...loaded.log.entries, entry], nowMs);
11078
11420
  mkdirSync(dirname(path), { recursive: true });
11079
11421
  const tmp = `${path}.tmp`;
11080
- writeFileSync(tmp, `${JSON.stringify({
11081
- version: CRO_LOG_VERSION,
11082
- entries: kept
11083
- }, null, 2)}\n`, "utf-8");
11422
+ writeFileSync(tmp, renderCroLogText(kept), "utf-8");
11084
11423
  renameSync(tmp, path);
11085
11424
  return {
11086
11425
  ok: true,
@@ -11704,10 +12043,12 @@ async function createAndBindTarget(ctx, root, dirName) {
11704
12043
  } }, "activate", {
11705
12044
  dirName,
11706
12045
  mdPath: join(root, "AGENT_SESSIONS", dirName, "SESSION.md"),
11707
- note: "auto-created by send-now"
12046
+ note: "auto-created by send-now",
12047
+ createdAsUser: true
11708
12048
  });
12049
+ const openingNote = deliverCarrierOpening(agent, buildCarrierOpening(parseTrajectoryCode(dirName) ?? dirName, trajectoryDescOf(dirName))) ? ", opening-as-user" : ", ⚠️ 开场白未投递";
11709
12050
  const wsNote = workspaceId !== void 0 ? "" : await attachToWorkspace(ctx, root, sessionId);
11710
- const how = `auto-create(${sessionId}${bound ? ", bound" : ", ⚠️ 绑定写入失败"}${workspaceId !== void 0 ? ", via-workspaceId(=用户同一形态)" : ", via-cwd+attach"}${wsNote})`;
12051
+ const how = `auto-create(${sessionId}${bound ? ", bound" : ", ⚠️ 绑定写入失败"}${workspaceId !== void 0 ? ", via-workspaceId(=用户同一形态)" : ", via-cwd+attach"}${openingNote}${wsNote})`;
11711
12052
  return {
11712
12053
  agent,
11713
12054
  how
@@ -12476,6 +12817,12 @@ function createCognitiveTrajectoryTool(ctx) {
12476
12817
  "rebuild requires --summary (next-phase summary ≤20 chars) + optional --note (task focus for the rebuilt self).",
12477
12818
  "send-later requires target (a code or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message.",
12478
12819
  "send-now requires target + message.",
12820
+ "AUTHORIZATION (read this before creating anything): creating a cognitive trajectory or a dsh carrier costs real work,",
12821
+ "and both are meant to serve a human intent — not to be spawned speculatively. Call create only when the user asked for",
12822
+ "a new line of work (or confirmed it), never to \"tidy up\" or to pre-provision something nobody requested.",
12823
+ "send-now creating a missing carrier on demand is likewise acceptable ONLY for a delivery the user actually wants made;",
12824
+ "it is not a way to start work nobody asked for. When in doubt, ask the user instead of creating.",
12825
+ "⚠️ This is a behavioral rule, not a mechanical gate: nothing stops a careless call. Treat it as etiquette with a cost.",
12479
12826
  "uid: READS a legacy TRAJECTORY-UID only — setting one is retired (codes replaced it). Use create --id instead.",
12480
12827
  "The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the code and date stay server-derived unless you pass --id."
12481
12828
  ].join(" "),
@@ -12512,7 +12859,7 @@ function createCognitiveTrajectoryTool(ctx) {
12512
12859
  },
12513
12860
  desc: {
12514
12861
  type: "string",
12515
- description: "create: short description (any language, ≤5 words; mutually exclusive with issue)"
12862
+ description: "create: short description (any language, ≤5 words; mutually exclusive with issue). 🔴 AUTHORIZATION: create is user-intent-driven — do not create a trajectory the user did not ask for (see the tool description)."
12516
12863
  },
12517
12864
  id: {
12518
12865
  type: "string",
@@ -12548,7 +12895,7 @@ function createCognitiveTrajectoryTool(ctx) {
12548
12895
  },
12549
12896
  message: {
12550
12897
  type: "string",
12551
- description: "send-later/send-now: the single message delivered to the target cognitive trajectory"
12898
+ description: "send-later/send-now: the single message delivered to the target cognitive trajectory. send-now may create and bind a carrier when the target has no live session — acceptable only for a delivery the user actually wants made (user-intent-driven), not for speculatively starting work."
12552
12899
  }
12553
12900
  },
12554
12901
  output: {
@@ -12823,7 +13170,7 @@ const localstoreTool = defineTool({
12823
13170
  });
12824
13171
  //#endregion
12825
13172
  //#region src/tools/trajectory-constraint-admin.ts
12826
- /** 轨迹约束 定义教程:概念 / `trajectory-constraint.json` schema / 双白名单 / 轨迹纪律 / 微信路由 */
13173
+ /** 轨迹约束 定义教程:概念 / `trajectory.json` schema / 双白名单 / 轨迹纪律 / 微信路由 */
12827
13174
  const TRAJECTORY_CONSTRAINT_GUIDE = `═══ 轨迹约束 Guide (2026-10-19 redesigned) ═══
12828
13175
 
12829
13176
  **A 轨迹约束 IS a cognitive trajectory — the one whose directory contains \`${TRAJECTORY_CONSTRAINT_FILENAME}\`.**
@@ -12832,8 +13179,12 @@ becomes a 轨迹约束; delete it and it goes back to being an ordinary trajecto
12832
13179
 
12833
13180
  AGENT_SESSIONS/<YYYY-MM-DD--<code>--<desc>>/
12834
13181
  ├─ SESSION.md ← its persistent body (this IS the workspace — nothing is auto-created)
12835
- ├─ ${TRAJECTORY_CONSTRAINT_FILENAME} ← its constraints (this file is what makes it a 轨迹约束)
12836
- └─ TRAJECTORY-UID ← legacy, read-only (superseded by the code)
13182
+ ├─ PROMPT.md ← its charter (trajectory-level prompt; all trajectories may have one)
13183
+ ├─ ${TRAJECTORY_CONSTRAINT_FILENAME} ← its declaration (this file is what makes it a 轨迹约束)
13184
+ └─ continuous-re-occurrence.ts ← optional CRO program (spawned, never imported)
13185
+
13186
+ The older names ${TRAJECTORY_LEGACY_FILENAMES.map((n) => `\`${n}\``).join(" / ")} are still READ
13187
+ (the family is checked in that order) — writing goes to \`${TRAJECTORY_FILENAME}\` only.
12837
13188
 
12838
13189
  ── ${TRAJECTORY_CONSTRAINT_FILENAME} schema ──
12839
13190
  {
@@ -12841,8 +13192,10 @@ becomes a 轨迹约束; delete it and it goes back to being an ordinary trajecto
12841
13192
  "tools": ["read","grep","msm",...], // non-MSM toolbar whitelist — anything not listed is REMOVED from the schema
12842
13193
  "msms": ["msm-a","msm-b"], // MSM whitelist — msm exec may only run these; register/deregister always refused
12843
13194
  "cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, // defaults TRUE (see below)
12844
- "systemPromptFile": ".opencode/skiff/<role>.md", // recommended: relative to CCC root, hot-reloads on save
12845
- "systemPrompt": "..." // inline fallback; systemPromptFile wins when both are set
13195
+ "systemPromptFile": ".opencode/skiff/<role>.md", // relative to CCC root, hot-reloads on save
13196
+ "systemPrompt": "...", // inline fallback; systemPromptFile wins when both are set
13197
+ "uid": "my-handle", // optional legacy alias (read-only; the code is the identity now)
13198
+ "cro": { "host": "<machine name>" } // optional: which machine may run this trajectory's CRO program
12846
13199
  }
12847
13200
 
12848
13201
  ── Two whitelists, both default-deny ──
@@ -12945,39 +13298,6 @@ function applyTrajectoryConstraint(root) {
12945
13298
  };
12946
13299
  }
12947
13300
  //#endregion
12948
- //#region src/tools/config-docs.ts
12949
- /**
12950
- * 渲染**目录**(第一层)。
12951
- *
12952
- * 🔴 只调 `summary`,**绝不调 `render`** —— 目录若触发正文,就退化成"整份长文"。
12953
- * 这条不是性能优化,是**判据**:它保证 `docs` 的输出大小与章数成正比,与正文长度无关。
12954
- */
12955
- function renderDocsIndex(chapters) {
12956
- const lines = [
12957
- "═══ CCC 配置说明 · 目录 ═══",
12958
- "",
12959
- "说明类内容已超过单次返回的合适篇幅 ⇒ 先读目录,再按章取详情。",
12960
- "",
12961
- "用法:container_admin config docs <章节>",
12962
- ""
12963
- ];
12964
- for (const ch of chapters) lines.push(` ${ch.id.padEnd(14)}${ch.summary}`);
12965
- lines.push("");
12966
- lines.push("相关:container_admin msm catalog(工具/能力目录——与本章录互指,不互相复制)");
12967
- return lines.join("\n");
12968
- }
12969
- /**
12970
- * 解析一个章节参数(第二层)。
12971
- *
12972
- * 不存在 ⇒ 返回 `null`,由调用方给出**带候选清单**的拒绝 ——
12973
- * "不知道这个章节" 与 "这一章是空的" 必须可区分(否则用户以为功能没有)。
12974
- */
12975
- function findDocsChapter(chapters, id) {
12976
- const want = id.trim().toLowerCase();
12977
- if (want === "") return null;
12978
- return chapters.find((c) => c.id.toLowerCase() === want) ?? null;
12979
- }
12980
- //#endregion
12981
13301
  //#region src/tools/container-admin.ts
12982
13302
  /**
12983
13303
  * container-admin.ts — container_admin(v1.30:机务舱——容器管理面统一入口)
@@ -13008,6 +13328,57 @@ function findDocsChapter(chapters, id) {
13008
13328
  function agentSessionId(exec) {
13009
13329
  return exec.agent?.session?.id ?? "";
13010
13330
  }
13331
+ /**
13332
+ * `config view` 的**现值摘要**(v1.75.0 起:不再回整份说明正文)。
13333
+ *
13334
+ * 语义边界(这正是它和 `docs config` 的分工):
13335
+ * - 本函数回答「**你这个 CCC 现在填了什么**」⇒ 只报有值的键,且值超长截断。
13336
+ * - 它回答不了「**这个键该怎么填**」⇒ 那是 `docs config` 那一章。
13337
+ *
13338
+ * 🔴 为什么必须截断:现值里可能有很长的字段(`topPrompt` 实测数百字符)。
13339
+ * 不截断会让 "看现值" 退化成 "又返回一坨正文" —— 那就回到了统一前的问题。
13340
+ *
13341
+ * ⚠️ 返回类型是 `JsonValue`(工具 output schema 的契约)⇒ 必须**转成纯 JSON 可表达形态**,
13342
+ * 不能直接把 `Record<string, unknown>` 塞回去(`unknown` 里可能有函数/undefined,
13343
+ * 那会让序列化层行为不确定)。
13344
+ */
13345
+ function viewConfigSummary(root) {
13346
+ let cfg;
13347
+ try {
13348
+ cfg = loadSerenityConfig(root);
13349
+ } catch (err) {
13350
+ return { error: `配置读取失败:${String(err?.message ?? err)}` };
13351
+ }
13352
+ const MAX_STR = 120;
13353
+ const MAX_ARRAY = 12;
13354
+ const MAX_KEYS = 12;
13355
+ /** 递归转成纯 JSON 可表达值(超长按上限截断,并标注原始规模) */
13356
+ const toJson = (v) => {
13357
+ if (v === null || v === void 0) return null;
13358
+ if (typeof v === "string") return v.length > MAX_STR ? `${v.slice(0, MAX_STR)}…(共 ${v.length} 字符)` : v;
13359
+ if (typeof v === "number") return Number.isFinite(v) ? v : String(v);
13360
+ if (typeof v === "boolean") return v;
13361
+ if (Array.isArray(v)) {
13362
+ const head = v.slice(0, MAX_ARRAY).map(toJson);
13363
+ return v.length > MAX_ARRAY ? [...head, `…(共 ${v.length} 项)`] : head;
13364
+ }
13365
+ if (typeof v === "object") {
13366
+ const obj = v;
13367
+ const keys = Object.keys(obj);
13368
+ const out = {};
13369
+ for (const k of keys.slice(0, MAX_KEYS)) out[k] = toJson(obj[k]);
13370
+ if (keys.length > MAX_KEYS) out["…"] = `另有 ${keys.length - MAX_KEYS} 个键`;
13371
+ return out;
13372
+ }
13373
+ return String(v);
13374
+ };
13375
+ return {
13376
+ cccRoot: root,
13377
+ sections: Object.keys(cfg),
13378
+ values: toJson(cfg),
13379
+ note: "这是**现值摘要**(只报有值的键、超长已截断)。每段怎么填、填错会怎样 ⇒ container_admin config docs config"
13380
+ };
13381
+ }
13011
13382
  function renderText$1(value) {
13012
13383
  return [{
13013
13384
  type: "text",
@@ -13036,21 +13407,49 @@ const DOMAIN_NAMES = Object.keys({
13036
13407
  ]
13037
13408
  });
13038
13409
  /**
13039
- * 说明目录的章节(P3 两层菜单的**唯一注册处**)。
13040
- *
13041
- * 🔴 加一章 = 在这里加一项(`render` 惰性求值)。**正文不复制**:
13042
- * 每章的真相源就在它自己的函数里(`CCC_CONFIG_REFERENCE` / `TRAJECTORY_CONSTRAINT_GUIDE`)——
13043
- * 本表只做**切分与路由**,避免"同一段说明有第二份会漂的副本"。
13044
- */
13045
- const DOCS_CHAPTERS = [{
13046
- id: "config",
13047
- summary: "CCC 配置全表——9 段各是什么、怎么填、填错会怎样(.opencode/serenity.json)",
13048
- render: () => CCC_CONFIG_REFERENCE
13049
- }, {
13050
- id: "trajectory-constraint",
13051
- summary: "轨迹约束:一条轨迹怎么被约束(trajectory-constraint.json 的 schema + 双白名单)",
13052
- render: () => TRAJECTORY_CONSTRAINT_GUIDE
13053
- }];
13410
+ * 说明目录的章节(**说明类内容的唯一注册处**)。
13411
+ *
13412
+ * 🔴 两条纪律:
13413
+ * ① **加一章 = 在这里加一项**(`render` 惰性求值)——说明类内容增长时**不新增工具/子命令**。
13414
+ * ② **正文不复制**:每章的真相源就在它自己的函数里(`CCC_CONFIG_REFERENCE` /
13415
+ * `TRAJECTORY_CONSTRAINT_GUIDE` / `ACC_CATALOG`)——本表只做**切分与路由**。
13416
+ *
13417
+ * ## 🔴 2026-10-19 v1.75.0:说明面统一(owner 裁 Q3 = 指针)
13418
+ *
13419
+ * 统一前有 **5 个并列入口**,其中两组返回**同一份正文**:
13420
+ * `docs config` ≡ `msm ccc-config`|`docs trajectory-constraint` ≡ `config guide`|
13421
+ * `msm catalog`(域挂错了:它描述整个 ACC,却挂在 MSM 注册表下)。
13422
+ *
13423
+ * ⇒ 统一后的分工(**一个内容一个正文入口**):
13424
+ * - **`config docs <chapter>`** = 唯一正文入口
13425
+ * - `msm ccc-config` / `config guide` = **指针**(指向对应 chapter,不再返回正文)
13426
+ * - `msm catalog` = **指针**(指向 `catalog` 章)
13427
+ *
13428
+ * 判据依据:`praxis` 已经做对了这件事 —— `praxis eap` 返回正文、`praxis` 无参返回目录,
13429
+ * **没有第二个入口读同一份 EAP**。本表把同一形态搬到 config 域。
13430
+ */
13431
+ const DOCS_CHAPTERS = [
13432
+ {
13433
+ id: "catalog",
13434
+ summary: "ACC 能力总目录——7 个能力分区,每区一句话 + 去哪个子命令看详情",
13435
+ render: () => ACC_CATALOG
13436
+ },
13437
+ {
13438
+ id: "config",
13439
+ summary: "CCC 配置全表——9 段各是什么、怎么填、填错会怎样(.opencode/serenity.json)",
13440
+ render: () => CCC_CONFIG_REFERENCE
13441
+ },
13442
+ {
13443
+ id: "trajectory-constraint",
13444
+ summary: "轨迹约束:一条轨迹怎么被约束(trajectory.json 的 schema + 双白名单)",
13445
+ render: () => TRAJECTORY_CONSTRAINT_GUIDE
13446
+ }
13447
+ ];
13448
+ /**
13449
+ * 指针与章目的关系(v1.75.0):
13450
+ * 被降级的旧入口(`msm catalog` / `msm ccc-config` / `config guide`)统一走
13451
+ * `config-docs.ts` 的 `docsPointer()` —— **只有一处实现**,避免两域各写一份文案而漂移。
13452
+ */
13054
13453
  function createContainerAdminTool(ctx) {
13055
13454
  return defineTool({
13056
13455
  name: "container_admin",
@@ -13131,8 +13530,11 @@ function createContainerAdminTool(ctx) {
13131
13530
  };
13132
13531
  }
13133
13532
  case "view":
13134
- case "": return { config: runMsm(root, { action: "ccc-config" }) };
13135
- case "guide": return { guide: TRAJECTORY_CONSTRAINT_GUIDE };
13533
+ case "": return {
13534
+ config: viewConfigSummary(root),
13535
+ docs: "完整配置说明(每段怎么填、填错会怎样)→ container_admin config docs config"
13536
+ };
13537
+ case "guide": return { guide: docsPointer("trajectory-constraint", "轨迹约束教程") };
13136
13538
  case "validate": return validateTrajectoryConstraint(root);
13137
13539
  case "apply": return applyTrajectoryConstraint(root);
13138
13540
  case "list": return listTrajectoryConstraintSummary(root);
@@ -14824,10 +15226,10 @@ function registerKeeper(ctx, opts = {}) {
14824
15226
  const root = cccRootForCwd(cwd);
14825
15227
  if (!root) return next();
14826
15228
  const sessionId = exec.agent?.session?.id;
14827
- const 轨迹约束Keeper = trajectoryConstraintParticipates(root, sessionId, "keeper");
14828
- const 轨迹约束Rebuild = trajectoryConstraintParticipates(root, sessionId, "rebuild");
15229
+ const participatesKeeper = trajectoryConstraintParticipates(root, sessionId, "keeper");
15230
+ const participatesRebuild = trajectoryConstraintParticipates(root, sessionId, "rebuild");
14829
15231
  const tracker = trackerFor(exec);
14830
- const shouldRemind = 轨迹约束Keeper ? tracker.step(exec.name) : false;
15232
+ const shouldRemind = participatesKeeper ? tracker.step(exec.name) : false;
14831
15233
  const downstream = await next();
14832
15234
  try {
14833
15235
  recordToolUsage(root, exec.name, exec.arguments);
@@ -14840,7 +15242,7 @@ function registerKeeper(ctx, opts = {}) {
14840
15242
  text: reminderText(code)
14841
15243
  });
14842
15244
  }
14843
- if (轨迹约束Rebuild) {
15245
+ if (participatesRebuild) {
14844
15246
  const session = exec.agent?.session;
14845
15247
  if (session) {
14846
15248
  const pressure = readContextPressure(ctx, session);
@@ -15793,7 +16195,7 @@ function registerStatusApi(ctx, opts = {}) {
15793
16195
  });
15794
16196
  }
15795
16197
  const { saveWeixinRoutes } = await import("./route-COOFLTpU.js").then((n) => n.c);
15796
- const { findDirNameByCode } = await import("./profile-BVt84mbi.js").then((n) => n.i);
16198
+ const { findDirNameByCode } = await import("./profile-DplLGGqo.js").then((n) => n.i);
15797
16199
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
15798
16200
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
15799
16201
  return;
@@ -20776,7 +21178,7 @@ const MEDIA_MAX_BYTES = 20971520;
20776
21178
  */
20777
21179
  async function resolveCccRootByAlias(ctx, alias) {
20778
21180
  try {
20779
- const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
21181
+ const { listCccs } = await import("./ccc-roots-rwgzt_0F.js").then((n) => n.a);
20780
21182
  const entries = await listCccs(ctx);
20781
21183
  const byName = entries.find((e) => e.name === alias);
20782
21184
  if (byName) return byName.root;
@@ -20844,7 +21246,7 @@ async function resolveInboundTarget(ctx, input) {
20844
21246
  });
20845
21247
  return null;
20846
21248
  }
20847
- const { findDirNameByCode } = await import("./profile-BVt84mbi.js").then((n) => n.i);
21249
+ const { findDirNameByCode } = await import("./profile-DplLGGqo.js").then((n) => n.i);
20848
21250
  if (findDirNameByCode(root, target.role) === null) {
20849
21251
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
20850
21252
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -21656,7 +22058,7 @@ function syncHumanChannel(ctx) {
21656
22058
  (async () => {
21657
22059
  try {
21658
22060
  stopMachineLoops();
21659
- const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
22061
+ const { listCccs } = await import("./ccc-roots-rwgzt_0F.js").then((n) => n.a);
21660
22062
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
21661
22063
  } catch {}
21662
22064
  })();
@@ -21920,7 +22322,7 @@ function matchCcc(input, candidates) {
21920
22322
  }
21921
22323
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
21922
22324
  async function collectCandidates(ctx) {
21923
- const { listCccs } = await import("./ccc-roots-B2t_aM8L.js").then((n) => n.a);
22325
+ const { listCccs } = await import("./ccc-roots-rwgzt_0F.js").then((n) => n.a);
21924
22326
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
21925
22327
  const seen = /* @__PURE__ */ new Set();
21926
22328
  const out = [];