@shgroup/dsh-serenity-hooks 1.79.4 → 1.80.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.79.4",
3
+ "version": "1.80.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -227,7 +227,7 @@ async function listCccs(ctx, opts = {}) {
227
227
  if (typeof defaultRoot === "string" && defaultRoot !== "" && !c.roots.includes(defaultRoot)) c.roots.unshift(defaultRoot);
228
228
  let rolesOf = null;
229
229
  if (opts.withRoles === true) {
230
- const { listTrajectoryConstraints } = await import("./profile-5TQ_C1xi.js").then((n) => n.i);
230
+ const { listTrajectoryConstraints } = await import("./profile-DmWw2M0q.js").then((n) => n.i);
231
231
  rolesOf = (root) => {
232
232
  try {
233
233
  return listTrajectoryConstraints(root).map((p) => p.code);
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
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-DAwz9Li1.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-DYJvlQhc.js";
3
3
  import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
4
- import { A as sessionEvents, B as isoLocal, C as listSessions, E as parseTrajectoryCode, F as trajectoryDescOf, H as localHuman, I as useSession, L as TRAJECTORY_FILENAME, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as TRAJECTORY_LEGACY_FILENAMES, S as listActiveTrajectories, T as parseSessionContextFromEvents, U as localIdStamp, V as localFileStamp, _ as createSession, a as readTrajectoryConstraintAt, b as getActiveSessionInfo, c as trajectoryConstraintForSession, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, m as UID_FILENAME, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, r as listTrajectoryConstraints, s as replacesDefaultIdentity, t as TRAJECTORY_CONSTRAINT_FILENAME, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as readTrajectoryFile } from "./profile-5TQ_C1xi.js";
4
+ import { A as sessionEvents, B as isoLocal, C as listSessions, E as parseTrajectoryCode, F as trajectoryDescOf, H as localHuman, I as useSession, L as TRAJECTORY_FILENAME, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as TRAJECTORY_LEGACY_FILENAMES, S as listActiveTrajectories, T as parseSessionContextFromEvents, U as localIdStamp, V as localFileStamp, _ as createSession, a as readTrajectoryConstraintAt, b as getActiveSessionInfo, c as trajectoryConstraintForSession, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, m as UID_FILENAME, n as isTrajectoryConstrained, o as readTrajectoryConstraintByCode, r as listTrajectoryConstraints, s as replacesDefaultIdentity, t as TRAJECTORY_CONSTRAINT_FILENAME, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as readTrajectoryFile } from "./profile-DmWw2M0q.js";
5
5
  import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
6
- import { _ as ACC_VERSION, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as resolveConstraintSystemPrompt, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as findPackageRoot } from "./message-source-DK2CnXlh.js";
7
- import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-BYC1uWi0.js";
6
+ import { _ as ACC_VERSION, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as resolveConstraintSystemPrompt, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as findPackageRoot } from "./message-source-BYO-FyFP.js";
7
+ import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-sV-kc1gQ.js";
8
8
  import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
9
- import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-B-vn8j4g.js";
9
+ import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-DdYLVK3O.js";
10
10
  import { REGISTERED_TOOLS } from "./invariant.js";
11
11
  import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-Dr0SFD8A.js";
12
12
  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";
@@ -7114,102 +7114,6 @@ function createCheapWorkerTool(ctx) {
7114
7114
  });
7115
7115
  }
7116
7116
  //#endregion
7117
- //#region src/trajectory/alive.ts
7118
- /**
7119
- * trajectory/alive.ts — **"这个 dsh 会话还活着吗"的唯一判据**(owner 2026-10-20 裁决)
7120
- *
7121
- * ## owner 的原话(本模块的全部设计依据)
7122
- *
7123
- * > 「死应该包含归档判断;只有 ① 或者 ②且不在归档集 才算活」
7124
- * > 「use 只要被调用了,可以无条件覆盖绑定到当前会话(这说明当前会话肯定活着啊)」
7125
- *
7126
- * ## 为什么必须收成一个判据(在此之前有三个互不一致的判据)
7127
- *
7128
- * | # | 判据 | 旧实现位置 | 单独使用时的漏洞 |
7129
- * |---|---|---|---|
7130
- * | ① | 进程内 loaded agent | `hostAgents(ctx).get(id)` | 冷会话全判死 |
7131
- * | ② | 磁盘上有会话日志 | `hasSessionLogById()` | **归档后日志逐字节不动** ⇒ 归档会话判活 |
7132
- * | ③ | 宿主归档集 | `workspaceRegistry.archivedSessionIds` | 旧 dsp **零引用**(整个仓库 0 处) |
7133
- *
7134
- * 宿主的归档语义是**只往集合里加一个 id、不动日志**(`dsh-workspace` 的
7135
- * `archiveSession` 注释:「Archiving never touches workspace accounting」)。
7136
- * ⇒ 只问 ② 的实现在归档上**必然判错**,而那个错误没有任何机械信号。
7137
- *
7138
- * ## 判据(owner 给定,逐字实现)
7139
- *
7140
- * ```text
7141
- * 活 ⟺ ① (进程内 loaded ⇒ 一定在)
7142
- * ∨ (② ∧ 不在归档集) (日志在、且没人把它归档掉)
7143
- * ```
7144
- *
7145
- * 🔴 **一处对原话的收紧(必须说出来,不许默默改)**:owner 的 ① 未附加"不在归档集",
7146
- * 而本模块把**归档判定排在最前** —— 即 `归档 ∧ live ⇒ 判死`。理由:归档是**人对这条会话
7147
- * 的处置声明**(宿主会把它从所有分组面隐藏),若仍被当成载体,唤醒调度器会去驱动一条
7148
- * **使用者以为已经收起来**的会话。⚠️ 与字面规则的差异仅此一格;`use` 不受影响
7149
- * (见下方 §use 逃生门),因此真需要复活一条已归档会话时**仍然有受支持的路**。
7150
- *
7151
- * ## 🔴 `use` 的逃生门(owner 第二条,写在调用点)
7152
- *
7153
- * **`use` 不查存活**:它被调用这件事本身证明**当前会话活着**(本模块的被测对象是
7154
- * "别人写的那个 id",而 `use` 手里拿的是**自己**)。⇒ `use` 走无条件覆盖
7155
- * (`claimBinding` 的 `takeover`),因此**判据再严也不会把一条轨迹锁死**。
7156
- * 这条逃生门是判据可以取严的前提,两者必须一起存在。
7157
- *
7158
- * ## 依赖纪律
7159
- *
7160
- * 纯判据 `isSessionAlive` 零依赖、可单测;宿主面只在 `hostAliveProbe` 里,
7161
- * 且**不抛错**(服务缺失 ⇒ 该维度作废,而不是让 ACC 工具整块失败)。
7162
- */
7163
- /** 宿主侧的归档集读取(不可用 ⇒ 空集 = 该维度作废,**不是**把所有会话判死) */
7164
- function archivedIdsOf(ctx) {
7165
- try {
7166
- const ids = hostService(ctx, "workspaceRegistry")?.archivedSessionIds;
7167
- return new Set(Array.isArray(ids) ? ids : []);
7168
- } catch {
7169
- return /* @__PURE__ */ new Set();
7170
- }
7171
- }
7172
- /**
7173
- * **宿主面的判活探针**(`(sessionId) => boolean`)——所有需要"这条会话还活着吗"的
7174
- * 生产代码都应当用这一个,不要自己拼 `agents.get` + `hasSessionLogById`。
7175
- *
7176
- * 语义 = {@link isSessionAlive},其中:
7177
- * · `liveIds` 因为宿主只给 `get(id)` 而**按 id 逐次问**(不是集合);
7178
- * · `archivedIds` 在**首次调用时**取一次快照并在闭包内复用(每 tick 一个探针,
7179
- * 归档集在一个 tick 内不变)—— 避免为每条轨迹重复读存储域。
7180
- *
7181
- * ⚠️ **快照的代价**:同一探针内若有人中途归档了某会话,本探针直到下次新建才看见。
7182
- * 调度器是每 tick 新建一个探针(见 `wake/scheduler.ts`),故实际窗口 ≤1 tick。
7183
- *
7184
- * @param ctx 插件上下文(取 `agents` 与 `workspaceRegistry`)
7185
- * @returns 探针;**永不抛**
7186
- */
7187
- function hostAliveProbe(ctx) {
7188
- let archived;
7189
- const archivedOnce = () => archived ??= archivedIdsOf(ctx);
7190
- return (sessionId) => {
7191
- if (typeof sessionId !== "string" || sessionId === "") return false;
7192
- if (archivedOnce().has(sessionId)) return false;
7193
- try {
7194
- if (hostAgents(ctx)?.get?.(sessionId)) return true;
7195
- } catch {}
7196
- try {
7197
- return hasSessionLogById(sessionsRootDir(), sessionId);
7198
- } catch {
7199
- return true;
7200
- }
7201
- };
7202
- }
7203
- /**
7204
- * 判据只有**两个**出口,别再开第三个:
7205
- * · {@link isSessionAlive} —— 纯判据(给定事实),单测的靶子;
7206
- * · {@link hostAliveProbe} —— 生产入口(自己去宿主与磁盘取事实)。
7207
- *
7208
- * ⚠️ 曾有一个 `aliveFactsFor`(把探针摊平成 `SessionAliveFacts`),**全仓零调用点**,
7209
- * 已删除 —— 留一个"看起来有用、实际无人调用"的导出,会让下一个人以为存在第二条判活路径
7210
- * (本仓同族教训:`cheapWorkerPresetInheritance` 全仓零调用点 ⇒ 接线断了却看不出来)。
7211
- */
7212
- //#endregion
7213
7117
  //#region src/wake/registry.ts
7214
7118
  /**
7215
7119
  * wake/registry.ts — 认知轨迹唤醒注册表(D58,S142 2026-09-13)
@@ -7670,6 +7574,102 @@ function deliverCarrierOpening(agent, text) {
7670
7574
  }
7671
7575
  }
7672
7576
  //#endregion
7577
+ //#region src/trajectory/alive.ts
7578
+ /**
7579
+ * trajectory/alive.ts — **"这个 dsh 会话还活着吗"的唯一判据**(owner 2026-10-20 裁决)
7580
+ *
7581
+ * ## owner 的原话(本模块的全部设计依据)
7582
+ *
7583
+ * > 「死应该包含归档判断;只有 ① 或者 ②且不在归档集 才算活」
7584
+ * > 「use 只要被调用了,可以无条件覆盖绑定到当前会话(这说明当前会话肯定活着啊)」
7585
+ *
7586
+ * ## 为什么必须收成一个判据(在此之前有三个互不一致的判据)
7587
+ *
7588
+ * | # | 判据 | 旧实现位置 | 单独使用时的漏洞 |
7589
+ * |---|---|---|---|
7590
+ * | ① | 进程内 loaded agent | `hostAgents(ctx).get(id)` | 冷会话全判死 |
7591
+ * | ② | 磁盘上有会话日志 | `hasSessionLogById()` | **归档后日志逐字节不动** ⇒ 归档会话判活 |
7592
+ * | ③ | 宿主归档集 | `workspaceRegistry.archivedSessionIds` | 旧 dsp **零引用**(整个仓库 0 处) |
7593
+ *
7594
+ * 宿主的归档语义是**只往集合里加一个 id、不动日志**(`dsh-workspace` 的
7595
+ * `archiveSession` 注释:「Archiving never touches workspace accounting」)。
7596
+ * ⇒ 只问 ② 的实现在归档上**必然判错**,而那个错误没有任何机械信号。
7597
+ *
7598
+ * ## 判据(owner 给定,逐字实现)
7599
+ *
7600
+ * ```text
7601
+ * 活 ⟺ ① (进程内 loaded ⇒ 一定在)
7602
+ * ∨ (② ∧ 不在归档集) (日志在、且没人把它归档掉)
7603
+ * ```
7604
+ *
7605
+ * 🔴 **一处对原话的收紧(必须说出来,不许默默改)**:owner 的 ① 未附加"不在归档集",
7606
+ * 而本模块把**归档判定排在最前** —— 即 `归档 ∧ live ⇒ 判死`。理由:归档是**人对这条会话
7607
+ * 的处置声明**(宿主会把它从所有分组面隐藏),若仍被当成载体,唤醒调度器会去驱动一条
7608
+ * **使用者以为已经收起来**的会话。⚠️ 与字面规则的差异仅此一格;`use` 不受影响
7609
+ * (见下方 §use 逃生门),因此真需要复活一条已归档会话时**仍然有受支持的路**。
7610
+ *
7611
+ * ## 🔴 `use` 的逃生门(owner 第二条,写在调用点)
7612
+ *
7613
+ * **`use` 不查存活**:它被调用这件事本身证明**当前会话活着**(本模块的被测对象是
7614
+ * "别人写的那个 id",而 `use` 手里拿的是**自己**)。⇒ `use` 走无条件覆盖
7615
+ * (`claimBinding` 的 `takeover`),因此**判据再严也不会把一条轨迹锁死**。
7616
+ * 这条逃生门是判据可以取严的前提,两者必须一起存在。
7617
+ *
7618
+ * ## 依赖纪律
7619
+ *
7620
+ * 纯判据 `isSessionAlive` 零依赖、可单测;宿主面只在 `hostAliveProbe` 里,
7621
+ * 且**不抛错**(服务缺失 ⇒ 该维度作废,而不是让 ACC 工具整块失败)。
7622
+ */
7623
+ /** 宿主侧的归档集读取(不可用 ⇒ 空集 = 该维度作废,**不是**把所有会话判死) */
7624
+ function archivedIdsOf(ctx) {
7625
+ try {
7626
+ const ids = hostService(ctx, "workspaceRegistry")?.archivedSessionIds;
7627
+ return new Set(Array.isArray(ids) ? ids : []);
7628
+ } catch {
7629
+ return /* @__PURE__ */ new Set();
7630
+ }
7631
+ }
7632
+ /**
7633
+ * **宿主面的判活探针**(`(sessionId) => boolean`)——所有需要"这条会话还活着吗"的
7634
+ * 生产代码都应当用这一个,不要自己拼 `agents.get` + `hasSessionLogById`。
7635
+ *
7636
+ * 语义 = {@link isSessionAlive},其中:
7637
+ * · `liveIds` 因为宿主只给 `get(id)` 而**按 id 逐次问**(不是集合);
7638
+ * · `archivedIds` 在**首次调用时**取一次快照并在闭包内复用(每 tick 一个探针,
7639
+ * 归档集在一个 tick 内不变)—— 避免为每条轨迹重复读存储域。
7640
+ *
7641
+ * ⚠️ **快照的代价**:同一探针内若有人中途归档了某会话,本探针直到下次新建才看见。
7642
+ * 调度器是每 tick 新建一个探针(见 `wake/scheduler.ts`),故实际窗口 ≤1 tick。
7643
+ *
7644
+ * @param ctx 插件上下文(取 `agents` 与 `workspaceRegistry`)
7645
+ * @returns 探针;**永不抛**
7646
+ */
7647
+ function hostAliveProbe(ctx) {
7648
+ let archived;
7649
+ const archivedOnce = () => archived ??= archivedIdsOf(ctx);
7650
+ return (sessionId) => {
7651
+ if (typeof sessionId !== "string" || sessionId === "") return false;
7652
+ if (archivedOnce().has(sessionId)) return false;
7653
+ try {
7654
+ if (hostAgents(ctx)?.get?.(sessionId)) return true;
7655
+ } catch {}
7656
+ try {
7657
+ return hasSessionLogById(sessionsRootDir(), sessionId);
7658
+ } catch {
7659
+ return true;
7660
+ }
7661
+ };
7662
+ }
7663
+ /**
7664
+ * 判据只有**两个**出口,别再开第三个:
7665
+ * · {@link isSessionAlive} —— 纯判据(给定事实),单测的靶子;
7666
+ * · {@link hostAliveProbe} —— 生产入口(自己去宿主与磁盘取事实)。
7667
+ *
7668
+ * ⚠️ 曾有一个 `aliveFactsFor`(把探针摊平成 `SessionAliveFacts`),**全仓零调用点**,
7669
+ * 已删除 —— 留一个"看起来有用、实际无人调用"的导出,会让下一个人以为存在第二条判活路径
7670
+ * (本仓同族教训:`cheapWorkerPresetInheritance` 全仓零调用点 ⇒ 接线断了却看不出来)。
7671
+ */
7672
+ //#endregion
7673
7673
  //#region src/cro/core.ts
7674
7674
  /**
7675
7675
  * cro/core.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
@@ -9462,7 +9462,8 @@ async function createAndBindTarget(ctx, root, dirName) {
9462
9462
  });
9463
9463
  const openingNote = deliverCarrierOpening(agent, buildCarrierOpening(parseTrajectoryCode(dirName) ?? dirName, trajectoryDescOf(dirName))) ? ", opening-as-user" : ", ⚠️ 开场白未投递";
9464
9464
  const wsNote = workspaceId !== void 0 ? "" : await attachToWorkspace(ctx, root, sessionId);
9465
- const how = `auto-create(${sessionId}${bound ? ", bound" : ", ⚠️ 绑定写入失败"}${workspaceId !== void 0 ? ", via-workspaceId(=用户同一形态)" : ", via-cwd+attach"}${openingNote}${wsNote})`;
9465
+ const viaNote = workspaceId !== void 0 ? ", via-workspaceId(=用户同一形态)" : ", via-cwd+attach";
9466
+ const how = `auto-create(${sessionId}${bound.ok ? ", bound" : `, ⚠️ 绑定写入失败(${bound.code})`}${viaNote}${openingNote}${wsNote})`;
9466
9467
  return {
9467
9468
  agent,
9468
9469
  how
@@ -10475,7 +10476,7 @@ function createCognitiveTrajectoryTool(ctx) {
10475
10476
  dirName: info.dirName,
10476
10477
  mdPath: info.mdPath,
10477
10478
  sessionId: info.sessionId,
10478
- alive: hostAliveProbe(ctx)
10479
+ takeover: true
10479
10480
  });
10480
10481
  const advisory = advisoryHint(targetEntry.dirName, join(targetEntry.path, "SESSION.md"));
10481
10482
  const out = {
@@ -10484,10 +10485,16 @@ function createCognitiveTrajectoryTool(ctx) {
10484
10485
  context: active.context
10485
10486
  };
10486
10487
  if (advisory) out.advisory = advisory;
10487
- if (bound === true) out.binding = "written";
10488
- else if (bound === false) {
10488
+ if (bound !== null && bound.ok) {
10489
+ out.binding = "written";
10490
+ if (bound.replaced !== void 0) {
10491
+ out.bindingReplaced = bound.replaced;
10492
+ out.bindingHint = `已顶掉原载体 ${bound.replaced}(owner 裁决:use 无条件覆盖到当前会话)。若那条会话还活着,它此刻**不再**承载「${info?.dirName ?? targetEntry.dirName}」——这是单槽位的必然结果,不是错误;需要它继续承载就再对它 \`use\` 一次。`;
10493
+ }
10494
+ } else if (bound !== null) {
10489
10495
  out.binding = "not-written";
10490
- out.bindingHint = `绑定未写入:本会话取不到可用的 header(id / cwd 缺一不可)⇒ 按契约「绑定声明是唯一真相」,「${info?.dirName ?? targetEntry.dirName}」此刻仍是**未绑定**。 会话标题变了**不能**当作绑定成功的证据(改名走的是另一条通路)。 详见宿主日志里的 \`✗ 绑定未写入(SESSION_HEADER_UNRESOLVED)\`。`;
10496
+ out.bindingCode = bound.code;
10497
+ out.bindingHint = `绑定未写入(${bound.code}):${bound.detail} ⇒ 按契约「绑定声明是唯一真相」,「${info?.dirName ?? targetEntry.dirName}」此刻仍是**未绑定**。 会话标题变了**不能**当作绑定成功的证据(改名走的是另一条通路)。`;
10491
10498
  }
10492
10499
  return out;
10493
10500
  }
@@ -10885,6 +10892,34 @@ const TRAJECTORY_SELF_GUIDE = [
10885
10892
  ⚠️ 与 SESSION.md 的分工(别写重了):
10886
10893
  SESSION.md = **发生了什么**(日志、决策、当前状态)
10887
10894
  PROMPT.md = **我是谁、怎么工作**(章程;很少改,改了就改变行为)`,
10895
+ "",
10896
+ `── 我的章程怎么进来:追加(缺省)还是替换 ──
10897
+
10898
+ \`trajectory.json\` 的 \`prompt\` 段(**不写它 = 追加**,既有行为逐字不变):
10899
+
10900
+ \`\`\`jsonc
10901
+ {
10902
+ "prompt": {
10903
+ "injection": "replace", // append(缺省)| replace
10904
+ "replaces": ["metaphor", "principles", "cce", "eap"] // 可选;不写 = 四块全换
10905
+ }
10906
+ }
10907
+ \`\`\`
10908
+
10909
+ | 模式 | 结果 |
10910
+ |---|---|
10911
+ | \`append\`(缺省) | 宁静号各块照常注入,我的 \`PROMPT.md\` **追加**在它们之后 |
10912
+ | \`replace\` | 我点名的**约束块**不再注入,我的 \`PROMPT.md\` **站到它们原来的位置**(紧跟身份块) |
10913
+
10914
+ 可替换的四块:\`metaphor\`(世界模型)/\`principles\`(信念与边界)/\`cce\`(连续性纪律)/\`eap\`(输出标准)。
10915
+ 🔴 **身份块(\`=== Serenity ACC ===\`)不可替换** —— 它回答"我在哪儿工作",不是"按什么章法工作";
10916
+ 写进去会被丢弃并指名告警。
10917
+
10918
+ 🔴 **两条硬前提**(缺一即**降级回 append** 并告警,绝不静默):
10919
+ ① \`replace\` 需要**真的有一份 \`PROMPT.md\`** —— 没有替代物的替换 = 静默清空纪律;
10920
+ ② \`append\` 下写 \`replaces\` 不生效(会告诉你,别以为它管用)。
10921
+
10922
+ ⚠️ 想表达"我一块都不想换" ⇒ 写 \`"replaces": []\`(显式空数组),它**不等于**不写(不写 = 四块全换)。`,
10888
10923
  "",
10889
10924
  `── 常驻:自己声明"欢迎别人来麻烦我"(trajectory.json 的 resident 字段)──
10890
10925
 
@@ -10923,6 +10958,7 @@ const TRAJECTORY_SELF_GUIDE = [
10923
10958
  | \`resident\` | 不出现在常驻名单里(不参与"被找") | 每轮 |
10924
10959
  | \`cognitiveTrajectory\` | 三项纪律全参与(true) | 每轮 |
10925
10960
  | \`systemPromptFile\` / \`systemPrompt\` | 用 ACC 的通用身份(**不替换**) | 保存即生效 |
10961
+ | \`prompt\` | 追加模式:宁静号各块照常注入,章程排在它们之后 | **每轮**(改文件下一轮即生效) |
10926
10962
 
10927
10963
  🔴 **"没写" 与 "写了空数组" 是两种意思**:
10928
10964
  \`"tools": []\`(显式空数组)= **我一个都不给** ⇒ 除 \`skill\` 外全部工具被隐藏。
@@ -10930,8 +10966,10 @@ const TRAJECTORY_SELF_GUIDE = [
10930
10966
  ⇒ 想只声明常驻?那就**只写 resident**,别的键一个都不要写。
10931
10967
 
10932
10968
  🔴 **写 \`trajectory.json\` 本身不改任何默认行为** —— 它只是"我把设定放这儿了"。
10933
- 只有当某个 key **真的取代了默认**(目前唯一一个是自带章程 systemPrompt*)时,
10934
- 这条轨迹才不再注入 ACC 身份。其余字段都不影响。`,
10969
+ 只有当某个 key **真的取代了默认**时才改变装配,目前只有两处:
10970
+ ① 自带章程 \`systemPromptFile\` / \`systemPrompt\`(取代 ACC 身份);
10971
+ ② \`prompt.injection = "replace"\`(**只**取代四个约束块,**不动**身份块)。
10972
+ 其余字段都不影响。`,
10935
10973
  "",
10936
10974
  `── 想限制自己(白名单 / 模型 / 纪律参与项)──
10937
10975
 
@@ -10940,6 +10978,8 @@ const TRAJECTORY_SELF_GUIDE = [
10940
10978
  `── 写完之后怎么验(别只看文档,要能机械判定)──
10941
10979
 
10942
10980
  章程生效了吗 改 PROMPT.md 后**在同一会话里**看下一轮的系统提示词段是否变化(保存即生效)
10981
+ 注入模式生效了吗 改 trajectory.json 的 prompt 段后,**同一会话下一轮**:replace 下四块消失、
10982
+ 我的章程紧跟身份块;⛔ 不需要重启(这两者都是每轮现读)
10943
10983
  我声明了什么 container_admin config list —— 逐轨迹列出 model/tools/msms 的**生效值**;
10944
10984
  没写的字段印 \`(unchanged)\` / \`(unrestricted)\`,不是伪装成某个默认值
10945
10985
  配置合法吗 container_admin config validate —— msms 均已注册 / model 在可用表里
@@ -14078,7 +14118,7 @@ function registerStatusApi(ctx, opts = {}) {
14078
14118
  });
14079
14119
  }
14080
14120
  const { saveWeixinRoutes } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
14081
- const { findDirNameByCode } = await import("./profile-5TQ_C1xi.js").then((n) => n.i);
14121
+ const { findDirNameByCode } = await import("./profile-DmWw2M0q.js").then((n) => n.i);
14082
14122
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
14083
14123
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
14084
14124
  return;
@@ -19075,7 +19115,7 @@ const MEDIA_MAX_BYTES = 20971520;
19075
19115
  */
19076
19116
  async function resolveCccRootByAlias(ctx, alias) {
19077
19117
  try {
19078
- const { listCccs } = await import("./ccc-roots-DAwz9Li1.js").then((n) => n.a);
19118
+ const { listCccs } = await import("./ccc-roots-DYJvlQhc.js").then((n) => n.a);
19079
19119
  const entries = await listCccs(ctx);
19080
19120
  const byName = entries.find((e) => e.name === alias);
19081
19121
  if (byName) return byName.root;
@@ -19143,7 +19183,7 @@ async function resolveInboundTarget(ctx, input) {
19143
19183
  });
19144
19184
  return null;
19145
19185
  }
19146
- const { findDirNameByCode } = await import("./profile-5TQ_C1xi.js").then((n) => n.i);
19186
+ const { findDirNameByCode } = await import("./profile-DmWw2M0q.js").then((n) => n.i);
19147
19187
  if (findDirNameByCode(root, target.role) === null) {
19148
19188
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
19149
19189
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -19983,7 +20023,7 @@ function syncHumanChannel(ctx) {
19983
20023
  (async () => {
19984
20024
  try {
19985
20025
  stopMachineLoops();
19986
- const { listCccs } = await import("./ccc-roots-DAwz9Li1.js").then((n) => n.a);
20026
+ const { listCccs } = await import("./ccc-roots-DYJvlQhc.js").then((n) => n.a);
19987
20027
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
19988
20028
  } catch {}
19989
20029
  })();
@@ -20247,7 +20287,7 @@ function matchCcc(input, candidates) {
20247
20287
  }
20248
20288
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
20249
20289
  async function collectCandidates(ctx) {
20250
- const { listCccs } = await import("./ccc-roots-DAwz9Li1.js").then((n) => n.a);
20290
+ const { listCccs } = await import("./ccc-roots-DYJvlQhc.js").then((n) => n.a);
20251
20291
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
20252
20292
  const seen = /* @__PURE__ */ new Set();
20253
20293
  const out = [];
@@ -1,6 +1,6 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-DAwz9Li1.js";
1
+ import { r as cccRootForCwd } from "./ccc-roots-DYJvlQhc.js";
2
2
  import { _ as resolveInside, g as readUtf8 } from "./ccc-CyYJL4F2.js";
3
- import { A as sessionEvents, E as parseTrajectoryCode, b as getActiveSessionInfo, j as sessionsRoot, x as getLastActiveSessionInfo } from "./profile-5TQ_C1xi.js";
3
+ import { A as sessionEvents, E as parseTrajectoryCode, b as getActiveSessionInfo, j as sessionsRoot, x as getLastActiveSessionInfo } from "./profile-DmWw2M0q.js";
4
4
  import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
@@ -521,7 +521,12 @@ function findTrajectoryHolding(sessionsRoot, sessionId) {
521
521
  * @param decl 要写入的声明
522
522
  * @param opts.takeover 允许顶掉本条轨迹现有的另一个载体
523
523
  * @param opts.alive 探针:**旧载体**是否还活着(缺省 ⇒ 一律当活 = 旧语义)
524
- * @returns `{ ok: true }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
524
+ * @returns `{ ok: true, replaced?, released? }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
525
+ *
526
+ * 🔴 **2026-10-20 追加 `replaced`(由一条 skiff 轨迹的实测逼出)**:`released` 只覆盖
527
+ * 「这个会话此前安家在**另一条**轨迹」那一格(跨轨迹清理);而**同一轨迹换载体**时被顶掉的那个
528
+ * 会话 id 此前**没有任何出口** ⇒ 调用方无法在回执里说出"你顶掉了谁"。
529
+ * 静默换代正是"一条轨迹感觉有两个载体"的成因 ⇒ 顶掉谁必须能被读出来。
525
530
  */
526
531
  function claimBinding(sessionsRoot, dirName, decl, opts = {}) {
527
532
  const holder = findTrajectoryHolding(sessionsRoot, decl.sessionId);
@@ -547,9 +552,11 @@ function claimBinding(sessionsRoot, dirName, decl, opts = {}) {
547
552
  code: "WRITE_FAILED",
548
553
  detail: `写 ${BIND_FILENAME} 失败:${join(sessionsRoot, dirName)}`
549
554
  };
550
- return released === void 0 ? { ok: true } : {
555
+ const replaced = cur !== null && cur.sessionId !== decl.sessionId ? cur.sessionId : void 0;
556
+ return {
551
557
  ok: true,
552
- released
558
+ ...replaced === void 0 ? {} : { replaced },
559
+ ...released === void 0 ? {} : { released }
553
560
  };
554
561
  }
555
562
  //#endregion
@@ -718,14 +725,26 @@ function listBoundSessionIds(root, dirName, alive) {
718
725
  * 旧签名 `appendBound(session, action, rec)` 保持不变:`session` 提供 `header.id` / `header.cwd`,
719
726
  * `rec.dirName` 指明写哪条轨迹的交代。
720
727
  *
721
- * @returns 写入成功 ⇒ true(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**)
728
+ * @returns 写入成功 ⇒ `{ ok: true, replaced?, released? }`(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**);
729
+ * 失败 ⇒ `{ ok: false, code, detail }`,**`code` 指名是哪一条守卫拒的**
730
+ *
731
+ * 🔴 **为什么从 `boolean` 改成分级结果(2026-10-20,一条 skiff 轨迹实测逼出)**:
732
+ * 本函数有**两条完全不同的失败路径** —— ① header 守卫(id / root 取不到)② `claimBinding` 拒绝
733
+ * (`SESSION_TAKEN` / `TRAJECTORY_OCCUPIED` / `WRITE_FAILED`)。日志侧早已分开(D186),
734
+ * **但回执侧只有一句 header 文案** ⇒ 调用方被指向错误的方向(那位实测者第一轮就照文案去找 header,
735
+ * 真因是槽位被占)。⇒ 结果必须**带码**,让回执能说出是哪一半。
722
736
  */
723
737
  function appendBound(session, action, rec) {
724
738
  const id = sessionHeader(session)?.id;
725
739
  const root = rootOf(session);
726
740
  if (typeof id !== "string" || id === "" || root === null) {
727
- console.warn(`[serenity-hooks] ✗ 绑定未写入(SESSION_HEADER_UNRESOLVED):会话对象取不到可用的 header(id=${typeof id === "string" && id !== "" ? id : "(空/非字符串)"},root=${root ?? "null(header.cwd 缺失或上溯不到 .serenity)"}) ⇒ 轨迹「${rec.dirName}」的 current.dsh_session_bind **未被改写**。 排查:该会话的 header.cwd 是否指向某个含 .serenity 的目录; 无 cwd 的会话(如某些程序性创建的载体)**不会**写绑定。`);
728
- return false;
741
+ const detail = `会话对象取不到可用的 header(id=${typeof id === "string" && id !== "" ? id : "(空/非字符串)"},root=${root ?? "null(header.cwd 缺失或上溯不到 .serenity)"}) ⇒ 轨迹「${rec.dirName}」的 current.dsh_session_bind **未被改写**。 排查:该会话的 header.cwd 是否指向某个含 .serenity 的目录; 无 cwd 的会话(如某些程序性创建的载体)**不会**写绑定。`;
742
+ console.warn(`[serenity-hooks] ✗ 绑定未写入(SESSION_HEADER_UNRESOLVED):${detail}`);
743
+ return {
744
+ ok: false,
745
+ code: "SESSION_HEADER_UNRESOLVED",
746
+ detail
747
+ };
729
748
  }
730
749
  const result = claimBinding(sessionsRoot(root), rec.dirName, {
731
750
  sessionId: id,
@@ -739,9 +758,17 @@ function appendBound(session, action, rec) {
739
758
  });
740
759
  if (!result.ok) {
741
760
  console.warn(`[serenity-hooks] ✗ 绑定未写入(${result.code}):${result.detail}`);
742
- return false;
761
+ return {
762
+ ok: false,
763
+ code: result.code,
764
+ detail: result.detail
765
+ };
743
766
  }
744
- return true;
767
+ return {
768
+ ok: true,
769
+ ...result.replaced === void 0 ? {} : { replaced: result.replaced },
770
+ ...result.released === void 0 ? {} : { released: result.released }
771
+ };
745
772
  }
746
773
  /**
747
774
  * 这条轨迹**是否曾经有过载体**(判"要不要允许自建")。
@@ -1078,6 +1078,18 @@ function readTrajectoryConstraintAt(trajectoryDir, root) {
1078
1078
  }
1079
1079
  const tools = strListMaybe(obj.tools);
1080
1080
  const msms = strListMaybe(obj.msms);
1081
+ /**
1082
+ * 提示词注入模式(owner 2026-10-20):只搬运,不判决。
1083
+ *
1084
+ * 🔴 判决在 `planPromptInjection()` —— 那里有唯一的合法性表与降级规则。
1085
+ * 这里若也判一次(比如把未知值归一成 append),就会有两处真相:
1086
+ * 一处静默改值、一处告警,读数是矛盾的(D151 同族)。
1087
+ */
1088
+ const promptRaw = obj.prompt;
1089
+ const prompt = promptRaw !== null && typeof promptRaw === "object" && !Array.isArray(promptRaw) ? {
1090
+ ...typeof promptRaw.injection === "string" ? { injection: promptRaw.injection } : {},
1091
+ ...strListMaybe(promptRaw.replaces) !== void 0 ? { replaces: strListMaybe(promptRaw.replaces) } : {}
1092
+ } : void 0;
1081
1093
  return {
1082
1094
  dirName,
1083
1095
  code,
@@ -1086,6 +1098,7 @@ function readTrajectoryConstraintAt(trajectoryDir, root) {
1086
1098
  ...tools !== void 0 ? { tools } : {},
1087
1099
  ...msms !== void 0 ? { msms } : {},
1088
1100
  ...resident !== void 0 ? { resident } : {},
1101
+ ...prompt !== void 0 ? { prompt } : {},
1089
1102
  cognitiveTrajectory: {
1090
1103
  session: boolOrTrue(ct.session),
1091
1104
  keeper: boolOrTrue(ct.keeper),
@@ -1,8 +1,8 @@
1
- import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-DAwz9Li1.js";
1
+ import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-DYJvlQhc.js";
2
2
  import { g as readUtf8, o as isSafeModeOn, p as readCognitiveTrajectorySkills, u as readBlacklist } from "./ccc-CyYJL4F2.js";
3
- import { B as isoLocal, D as readActiveSessionMd, a as readTrajectoryConstraintAt, c as trajectoryConstraintForSession, d as unregisterTrajectorySession$1, j as sessionsRoot, l as registerTrajectorySession$1, p as DEFAULT_SESSION_SCOPE, u as trajectoryDirNameForSession } from "./profile-5TQ_C1xi.js";
3
+ import { B as isoLocal, D as readActiveSessionMd, a as readTrajectoryConstraintAt, c as trajectoryConstraintForSession, d as unregisterTrajectorySession$1, j as sessionsRoot, l as registerTrajectorySession$1, p as DEFAULT_SESSION_SCOPE, u as trajectoryDirNameForSession } from "./profile-DmWw2M0q.js";
4
4
  import { a as readGitTrack, i as localstorePath } from "./service-DDmHSglV.js";
5
- import { _ as ACC_VERSION, d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt } from "./message-source-DK2CnXlh.js";
5
+ import { _ as ACC_VERSION, d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt } from "./message-source-BYO-FyFP.js";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -1379,6 +1379,110 @@ function applyWirePatch(wire) {
1379
1379
  return updateAdvancedSettings(patch);
1380
1380
  }
1381
1381
  //#endregion
1382
+ //#region src/trajectory/prompt-injection.ts
1383
+ /**
1384
+ * trajectory/prompt-injection.ts — **轨迹提示词的注入模式**(owner 2026-10-20 令)
1385
+ *
1386
+ * ## owner 的原话与动机(本模块的全部设计依据)
1387
+ *
1388
+ * > 「某条角色轨迹目前工作起来,容易多做多想,主要原因我认为还是宁静号原本的提示词注入导致的,
1389
+ * > 所以我认为轨迹提示词的设定要容许更高的复杂度,轨迹自身的配置里要有提示词注入选项:
1390
+ * > **默认追加**,可以设定为**替换宁静号系统提示词的约束部分**;同时要确认
1391
+ * > 对该提示词和设定的修改可以**热生效**。」
1392
+ *
1393
+ * ## 两个模式(判据只有这两个,别再开第三个)
1394
+ *
1395
+ * | 模式 | 行为 |
1396
+ * |---|---|
1397
+ * | `append`(**缺省**) | 容器与质量各块照常注入,轨迹提示词**追加**在第 ⑧ 位 —— 这是 2026-10-19 起的既有行为,**不变** |
1398
+ * | `replace` | 轨迹提示词**取代**指定的「约束块」,并站到它们原来的位置上(紧跟 ACC 身份) |
1399
+ *
1400
+ * ## 🔴 可替换的块 = 四个「约束块」,**身份块永不在列**
1401
+ *
1402
+ * `metaphor`(世界模型)/`principles`(信念与边界)/`cce`(连续性纪律)/`eap`(输出质量标准)。
1403
+ * **`identity`(`=== Serenity ACC ===`)刻意不可替换**:它回答的是"这条轨迹在哪里工作",
1404
+ * 不是"按什么章法工作";把它换掉会让同一个容器里的轨迹**彼此不再共享同一套边界**,
1405
+ * 而那正是 §5.9b 四层模型里"容器层永不可覆盖"那一条要保的东西。
1406
+ * ⚠️ 若以后确实要开放身份块,那是一次**标准级**决定(specs §5.9b),不是加个字符串。
1407
+ *
1408
+ * ## 三条失败语义(都**响亮**,不静默降级)
1409
+ *
1410
+ * 1. **未知块名** ⇒ 丢弃该项并指名("教一个会静默不生效的写法"是最坏的一类文档债)。
1411
+ * 2. **`replace` 但没有轨迹提示词** ⇒ **不执行替换**,按 `append` 走并告警 ——
1412
+ * 「替换」需要一个替代物;没有替代物的替换 = **静默清空纪律**,那不是本选项的意思。
1413
+ * 3. **`append` 却写了 `replaces`** ⇒ `replaces` 不生效并告警(同上:不教空头写法)。
1414
+ *
1415
+ * 本模块是**纯函数**(不读盘、不打日志):读盘在调用方(`seams/system-prompt.ts`),
1416
+ * 日志也由调用方按 `problems` 打 —— 这样判据可单测,且告警去重只在一个地方。
1417
+ */
1418
+ /** 注入模式的闭集(**新增模式 = 改标准**,不是加一个字符串值) */
1419
+ const PROMPT_INJECTION_MODES = ["append", "replace"];
1420
+ /**
1421
+ * 可被轨迹提示词取代的块(**有意排除 `identity`**,见文件头)。
1422
+ *
1423
+ * ⚠️ 这些 id 必须与 `seams/system-prompt.ts` 里实际的块一一对应;
1424
+ * 改块名/加块时**同批改这里**,否则本选项会静默失配(D143 同族)。
1425
+ */
1426
+ const REPLACEABLE_PROMPT_BLOCKS = [
1427
+ "metaphor",
1428
+ "principles",
1429
+ "cce",
1430
+ "eap"
1431
+ ];
1432
+ const DEFAULT_PLAN = {
1433
+ mode: "append",
1434
+ replaces: /* @__PURE__ */ new Set(),
1435
+ promptPosition: "late",
1436
+ problems: []
1437
+ };
1438
+ /**
1439
+ * 判决一次注入(纯函数)。
1440
+ *
1441
+ * @param raw `trajectory.json` 的 `prompt` 段(原样;`undefined` = 没写 = 缺省追加)
1442
+ * @param hasTrajectoryPrompt 该轨迹目录里**是否真有** `PROMPT.md`(调用方查,本函数不做 IO)
1443
+ */
1444
+ function planPromptInjection(raw, hasTrajectoryPrompt) {
1445
+ if (raw === void 0) return DEFAULT_PLAN;
1446
+ const problems = [];
1447
+ const declared = typeof raw.injection === "string" ? raw.injection.trim() : "";
1448
+ if (declared !== "" && !PROMPT_INJECTION_MODES.includes(declared)) {
1449
+ problems.push(`prompt.injection 的取值「${declared}」不是合法模式(只有 ${PROMPT_INJECTION_MODES.join(" / ")})⇒ 按 append 处理`);
1450
+ return {
1451
+ ...DEFAULT_PLAN,
1452
+ problems
1453
+ };
1454
+ }
1455
+ if ((declared === "replace" ? "replace" : "append") === "append") {
1456
+ if (raw.replaces !== void 0) problems.push("prompt.replaces 只在 injection=\"replace\" 下生效(当前是 append)⇒ 已忽略");
1457
+ return {
1458
+ ...DEFAULT_PLAN,
1459
+ problems
1460
+ };
1461
+ }
1462
+ if (!hasTrajectoryPrompt) {
1463
+ problems.push("prompt.injection=\"replace\" 但轨迹目录里没有 PROMPT.md ⇒ 不执行替换(按 append 处理):替换需要一个替代物,否则就是静默清空纪律");
1464
+ return {
1465
+ ...DEFAULT_PLAN,
1466
+ problems
1467
+ };
1468
+ }
1469
+ const wanted = raw.replaces;
1470
+ let picked;
1471
+ if (wanted === void 0) picked = [...REPLACEABLE_PROMPT_BLOCKS];
1472
+ else {
1473
+ const invalid = wanted.filter((n) => !REPLACEABLE_PROMPT_BLOCKS.includes(n));
1474
+ if (invalid.length > 0) problems.push(`prompt.replaces 含不可替换的块名:${invalid.join(" / ")}(可替换的只有 ${REPLACEABLE_PROMPT_BLOCKS.join(" / ")};身份块 identity 刻意不可替换)⇒ 已忽略这些项`);
1475
+ picked = wanted.filter((n) => REPLACEABLE_PROMPT_BLOCKS.includes(n));
1476
+ }
1477
+ const replaces = new Set(picked);
1478
+ return {
1479
+ mode: "replace",
1480
+ replaces,
1481
+ promptPosition: replaces.size === 0 ? "late" : "constraint",
1482
+ problems
1483
+ };
1484
+ }
1485
+ //#endregion
1382
1486
  //#region src/seams/system-prompt.ts
1383
1487
  /** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
1384
1488
  function readPersonaSettings() {
@@ -1805,13 +1909,15 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
1805
1909
  function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE, dirName) {
1806
1910
  const persona = readPersonaSettings();
1807
1911
  const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
1808
- const parts = [
1809
- identityBlock(root),
1810
- metaphorBlock(),
1811
- principlesBlock(root, personaOn),
1812
- cceBlock(),
1813
- eapBlock()
1814
- ];
1912
+ const trajReader = dirName === void 0 ? null : createTrajectoryPromptReader(root, dirName);
1913
+ const trajPrompt = trajReader === null ? "" : trajectoryPromptBlock(trajReader());
1914
+ const plan = planInjectionFor(root, dirName, trajPrompt !== "");
1915
+ const parts = [identityBlock(root)];
1916
+ if (plan.promptPosition === "constraint" && trajPrompt !== "") parts.push(trajPrompt);
1917
+ if (!plan.replaces.has("metaphor")) parts.push(metaphorBlock());
1918
+ if (!plan.replaces.has("principles")) parts.push(principlesBlock(root, personaOn));
1919
+ if (!plan.replaces.has("cce")) parts.push(cceBlock());
1920
+ if (!plan.replaces.has("eap")) parts.push(eapBlock());
1815
1921
  if (personaOn) {
1816
1922
  const expr = personaBlock(persona.mode, persona.overrideText);
1817
1923
  if (expr !== "") parts.push(expr);
@@ -1820,14 +1926,45 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE, dirName) {
1820
1926
  if (state) parts.push(state);
1821
1927
  const skill = entrySkillSectionText(root);
1822
1928
  if (skill) parts.push(skill);
1823
- const trajPrompt = dirName === void 0 ? "" : trajectoryPromptBlock(createTrajectoryPromptReader(root, dirName)());
1824
- if (trajPrompt !== "") parts.push(trajPrompt);
1929
+ if (plan.promptPosition === "late" && trajPrompt !== "") parts.push(trajPrompt);
1825
1930
  parts.push(toolsBlock());
1826
1931
  const session = sessionBlock(root, scope);
1827
1932
  if (session) parts.push(session);
1828
1933
  return parts.join("\n\n");
1829
1934
  }
1830
1935
  /**
1936
+ * 解析该轨迹的**注入判决**(每次装配现读;坏设定只告警一次,不阻断装配)。
1937
+ *
1938
+ * 🔴 为什么热路径可以读盘:`trajectory.json` 是**每轮都要读**的本来就有的东西
1939
+ * (工具白名单 / model / resident 都走同一条读取路径),多读一次不会改变复杂度量级;
1940
+ * 而**缓存**会让"改了设定但没生效"重新变成一个只能靠重启解释的现象 ——
1941
+ * owner 这次的原话里明确要求「对提示词和设定的修改可以热生效」。
1942
+ *
1943
+ * @param root CCC 根
1944
+ * @param dirName 绑定的轨迹目录名(`undefined` ⇒ 追加模式,无轨迹上下文)
1945
+ * @param hasTrajectoryPrompt 该轨迹目录里是否真有 `PROMPT.md`(`replace` 的前提)
1946
+ */
1947
+ function planInjectionFor(root, dirName, hasTrajectoryPrompt) {
1948
+ if (dirName === void 0) return planPromptInjection(void 0, false);
1949
+ let raw;
1950
+ try {
1951
+ raw = readTrajectoryConstraintAt(join(sessionsRoot(root), dirName), root)?.prompt;
1952
+ } catch {
1953
+ raw = void 0;
1954
+ }
1955
+ const plan = planPromptInjection(raw, hasTrajectoryPrompt);
1956
+ for (const p of plan.problems) {
1957
+ const key = `${root}\u0000${dirName}\u0000${p}`;
1958
+ if (!warnedPromptInjection.has(key)) {
1959
+ warnedPromptInjection.add(key);
1960
+ console.warn(`[serenity-hooks] ✗ 轨迹提示词注入设定(${dirName}):${p}`);
1961
+ }
1962
+ }
1963
+ return plan;
1964
+ }
1965
+ /** 同一(CCC × 轨迹 × 原因)只告警一次:每轮装配都执行,不去重会刷屏(但**不静默**) */
1966
+ const warnedPromptInjection = /* @__PURE__ */ new Set();
1967
+ /**
1831
1968
  * Code Mode 适配行:当前 scope 以 Code Mode 呈现工具时(run_code 可见),
1832
1969
  * ACC 块按 native 语义指引"直接调用工具"会与"只有 run_code 可直接调用"的执行
1833
1970
  * 塌缩冲突(模型直呼工具名 → UNKNOWN_TOOL,拒绝信息误导)。追加一行说明,
@@ -1,4 +1,4 @@
1
- import { f as hostSettings } from "./ccc-roots-DAwz9Li1.js";
1
+ import { f as hostSettings } from "./ccc-roots-DYJvlQhc.js";
2
2
  //#region src/surface/settings.ts
3
3
  /**
4
4
  * 设置面板命名空间 = **profile 条目 id**(v0.1.7 起;与旧 `settings.yaml` 段名逐字相同)。
@@ -241,13 +241,19 @@ export declare function findTrajectoryHolding(sessionsRoot: string, sessionId: s
241
241
  * @param decl 要写入的声明
242
242
  * @param opts.takeover 允许顶掉本条轨迹现有的另一个载体
243
243
  * @param opts.alive 探针:**旧载体**是否还活着(缺省 ⇒ 一律当活 = 旧语义)
244
- * @returns `{ ok: true }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
244
+ * @returns `{ ok: true, replaced?, released? }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
245
+ *
246
+ * 🔴 **2026-10-20 追加 `replaced`(由一条 skiff 轨迹的实测逼出)**:`released` 只覆盖
247
+ * 「这个会话此前安家在**另一条**轨迹」那一格(跨轨迹清理);而**同一轨迹换载体**时被顶掉的那个
248
+ * 会话 id 此前**没有任何出口** ⇒ 调用方无法在回执里说出"你顶掉了谁"。
249
+ * 静默换代正是"一条轨迹感觉有两个载体"的成因 ⇒ 顶掉谁必须能被读出来。
245
250
  */
246
251
  export declare function claimBinding(sessionsRoot: string, dirName: string, decl: BindDeclaration, opts?: {
247
252
  takeover?: boolean;
248
253
  alive?: (sessionId: string) => boolean;
249
254
  }): {
250
255
  ok: true;
256
+ replaced?: string;
251
257
  released?: string;
252
258
  } | {
253
259
  ok: false;
@@ -120,13 +120,37 @@ export declare function supersedeOtherBindings(_root: string, _dirName: string,
120
120
  * 保留本函数只为不打断既有调用点,恒返回空数组。
121
121
  */
122
122
  export declare function pruneMissingBindings(_root: string, _isMissing: (sessionId: string) => boolean): string[];
123
+ /**
124
+ * 绑定写入的结果(分级,**不是 boolean**)。
125
+ *
126
+ * 分级的意义 = **调用方能说出是哪一半失败**(见 `appendBound` 头注)。
127
+ */
128
+ export type BindWriteCode = 'SESSION_HEADER_UNRESOLVED' | 'SESSION_TAKEN' | 'TRAJECTORY_OCCUPIED' | 'WRITE_FAILED';
129
+ export type BindWriteResult = {
130
+ ok: true;
131
+ /** 同一轨迹上被顶掉的旧载体 id(`use` 无条件覆盖时要写进回执 —— 静默换代是不允许的) */
132
+ replaced?: string;
133
+ /** 被清掉的**另一条**轨迹的残留声明(`claimBinding` 的安全网) */
134
+ released?: string;
135
+ } | {
136
+ ok: false;
137
+ code: BindWriteCode;
138
+ detail: string;
139
+ };
123
140
  /**
124
141
  * 把一条轨迹的当前载体写成 `sessionId`(**唯一写入口**,内部走 `claimBinding` 的两条不变量)。
125
142
  *
126
143
  * 旧签名 `appendBound(session, action, rec)` 保持不变:`session` 提供 `header.id` / `header.cwd`,
127
144
  * `rec.dirName` 指明写哪条轨迹的交代。
128
145
  *
129
- * @returns 写入成功 ⇒ true(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**)
146
+ * @returns 写入成功 ⇒ `{ ok: true, replaced?, released? }`(**每条轨迹只有一个文件,没有"追加"语义,只有"覆盖"**);
147
+ * 失败 ⇒ `{ ok: false, code, detail }`,**`code` 指名是哪一条守卫拒的**
148
+ *
149
+ * 🔴 **为什么从 `boolean` 改成分级结果(2026-10-20,一条 skiff 轨迹实测逼出)**:
150
+ * 本函数有**两条完全不同的失败路径** —— ① header 守卫(id / root 取不到)② `claimBinding` 拒绝
151
+ * (`SESSION_TAKEN` / `TRAJECTORY_OCCUPIED` / `WRITE_FAILED`)。日志侧早已分开(D186),
152
+ * **但回执侧只有一句 header 文案** ⇒ 调用方被指向错误的方向(那位实测者第一轮就照文案去找 header,
153
+ * 真因是槽位被占)。⇒ 结果必须**带码**,让回执能说出是哪一半。
130
154
  */
131
155
  export declare function appendBound(session: unknown, action: SessionBoundAction, rec: {
132
156
  dirName: string;
@@ -144,7 +168,7 @@ export declare function appendBound(session: unknown, action: SessionBoundAction
144
168
  * 生产调用点应当传 {@link hostAliveProbe}(`trajectory/alive.ts`)。
145
169
  */
146
170
  alive?: (sessionId: string) => boolean;
147
- }): boolean;
171
+ }): BindWriteResult;
148
172
  /**
149
173
  * 这条轨迹**是否曾经有过载体**(判"要不要允许自建")。
150
174
  *
@@ -31,6 +31,7 @@
31
31
  * ⚠️ **文件名 `profile` 与 `trajectory-constraint.json` / `TrajectoryConstraint` 等符号名一律不动**:
32
32
  * 那是有具体指称的东西(一个固定文件名 + 它的解析结果),不是组织单位。
33
33
  */
34
+ import type { RawPromptInjection } from '../prompt-injection.js';
34
35
  /**
35
36
  * 🆕 **声明文件的文件名**(2026-10-19 owner 裁 Q2:短名 `trajectory.json`)。
36
37
  *
@@ -86,6 +87,18 @@ export interface TrajectoryConstraint {
86
87
  resident?: {
87
88
  serves?: string;
88
89
  };
90
+ /**
91
+ * **提示词注入模式**(owner 2026-10-20 令)—— 轨迹提示词**怎么**进入系统提示词。
92
+ *
93
+ * - 缺省(字段不写)⇒ `append`:容器与质量各块照常注入,轨迹提示词追加在第 ⑧ 位。
94
+ * - `{ "injection": "replace", "replaces": [...] }` ⇒ 轨迹提示词**取代**指定的约束块
95
+ * (`metaphor` / `principles` / `cce` / `eap`;缺省全换),并站到它们的原位上。
96
+ * **身份块(`=== Serenity ACC ===`)刻意不可替换** —— 判据见 `trajectory/prompt-injection.ts`。
97
+ *
98
+ * ⚠️ 本字段**只搬运值**,合法性与降级判决在 `planPromptInjection()`(纯函数、可单测);
99
+ * 两份实现会让"文件里写的"与"实际生效的"分叉。
100
+ */
101
+ prompt?: RawPromptInjection;
89
102
  }
90
103
  /**
91
104
  * 读一个目录下的 `trajectory-constraint.json`(**纯读,不施加约束**)。
@@ -0,0 +1,73 @@
1
+ /**
2
+ * trajectory/prompt-injection.ts — **轨迹提示词的注入模式**(owner 2026-10-20 令)
3
+ *
4
+ * ## owner 的原话与动机(本模块的全部设计依据)
5
+ *
6
+ * > 「某条角色轨迹目前工作起来,容易多做多想,主要原因我认为还是宁静号原本的提示词注入导致的,
7
+ * > 所以我认为轨迹提示词的设定要容许更高的复杂度,轨迹自身的配置里要有提示词注入选项:
8
+ * > **默认追加**,可以设定为**替换宁静号系统提示词的约束部分**;同时要确认
9
+ * > 对该提示词和设定的修改可以**热生效**。」
10
+ *
11
+ * ## 两个模式(判据只有这两个,别再开第三个)
12
+ *
13
+ * | 模式 | 行为 |
14
+ * |---|---|
15
+ * | `append`(**缺省**) | 容器与质量各块照常注入,轨迹提示词**追加**在第 ⑧ 位 —— 这是 2026-10-19 起的既有行为,**不变** |
16
+ * | `replace` | 轨迹提示词**取代**指定的「约束块」,并站到它们原来的位置上(紧跟 ACC 身份) |
17
+ *
18
+ * ## 🔴 可替换的块 = 四个「约束块」,**身份块永不在列**
19
+ *
20
+ * `metaphor`(世界模型)/`principles`(信念与边界)/`cce`(连续性纪律)/`eap`(输出质量标准)。
21
+ * **`identity`(`=== Serenity ACC ===`)刻意不可替换**:它回答的是"这条轨迹在哪里工作",
22
+ * 不是"按什么章法工作";把它换掉会让同一个容器里的轨迹**彼此不再共享同一套边界**,
23
+ * 而那正是 §5.9b 四层模型里"容器层永不可覆盖"那一条要保的东西。
24
+ * ⚠️ 若以后确实要开放身份块,那是一次**标准级**决定(specs §5.9b),不是加个字符串。
25
+ *
26
+ * ## 三条失败语义(都**响亮**,不静默降级)
27
+ *
28
+ * 1. **未知块名** ⇒ 丢弃该项并指名("教一个会静默不生效的写法"是最坏的一类文档债)。
29
+ * 2. **`replace` 但没有轨迹提示词** ⇒ **不执行替换**,按 `append` 走并告警 ——
30
+ * 「替换」需要一个替代物;没有替代物的替换 = **静默清空纪律**,那不是本选项的意思。
31
+ * 3. **`append` 却写了 `replaces`** ⇒ `replaces` 不生效并告警(同上:不教空头写法)。
32
+ *
33
+ * 本模块是**纯函数**(不读盘、不打日志):读盘在调用方(`seams/system-prompt.ts`),
34
+ * 日志也由调用方按 `problems` 打 —— 这样判据可单测,且告警去重只在一个地方。
35
+ */
36
+ /** 注入模式的闭集(**新增模式 = 改标准**,不是加一个字符串值) */
37
+ export declare const PROMPT_INJECTION_MODES: readonly ["append", "replace"];
38
+ export type PromptInjectionMode = (typeof PROMPT_INJECTION_MODES)[number];
39
+ /**
40
+ * 可被轨迹提示词取代的块(**有意排除 `identity`**,见文件头)。
41
+ *
42
+ * ⚠️ 这些 id 必须与 `seams/system-prompt.ts` 里实际的块一一对应;
43
+ * 改块名/加块时**同批改这里**,否则本选项会静默失配(D143 同族)。
44
+ */
45
+ export declare const REPLACEABLE_PROMPT_BLOCKS: readonly ["metaphor", "principles", "cce", "eap"];
46
+ export type ReplaceablePromptBlock = (typeof REPLACEABLE_PROMPT_BLOCKS)[number];
47
+ /** `trajectory.json` 里 `prompt` 段的**原始**形态(校验在本模块做,profile 只管搬运) */
48
+ export interface RawPromptInjection {
49
+ injection?: string;
50
+ replaces?: string[];
51
+ }
52
+ /** 一次装配要用的**判决**(纯数据;调用方据此决定注入哪些块、提示词放哪) */
53
+ export interface PromptInjectionPlan {
54
+ /** 实际生效的模式(`replace` 的前提不成立时会被降级成 `append`) */
55
+ mode: PromptInjectionMode;
56
+ /** `replace` 时**真正**被取代的块(`append` ⇒ 空集) */
57
+ replaces: ReadonlySet<ReplaceablePromptBlock>;
58
+ /**
59
+ * 轨迹提示词的位置:
60
+ * · `constraint` —— 站到被取代块的原位(紧跟身份)
61
+ * · `late` —— 第 ⑧ 位(追加形态,既有行为)
62
+ */
63
+ promptPosition: 'constraint' | 'late';
64
+ /** 需要**响亮**告警的原因(空数组 = 一切正常)。调用方负责按轨迹去重后打日志 */
65
+ problems: string[];
66
+ }
67
+ /**
68
+ * 判决一次注入(纯函数)。
69
+ *
70
+ * @param raw `trajectory.json` 的 `prompt` 段(原样;`undefined` = 没写 = 缺省追加)
71
+ * @param hasTrajectoryPrompt 该轨迹目录里**是否真有** `PROMPT.md`(调用方查,本函数不做 IO)
72
+ */
73
+ export declare function planPromptInjection(raw: RawPromptInjection | undefined, hasTrajectoryPrompt: boolean): PromptInjectionPlan;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.79.4",
3
+ "version": "1.80.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {