@shgroup/dsh-serenity-hooks 1.77.1 → 1.78.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,12 +1,14 @@
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-rwgzt_0F.js";
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, 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
- 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-f9WZXcgi.js";
7
- import { a as listBoundSessionIds, i as carrierState, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-CYa8LbbM.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-D3ty1C8P.js";
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-C7BuyZK7.js";
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-BOG0piL8.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-BxDVrxPH.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-Bb4OnEdI.js";
9
10
  import { REGISTERED_TOOLS } from "./invariant.js";
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";
10
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";
11
13
  import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
12
14
  import { createRequire } from "node:module";
@@ -20,7 +22,7 @@ import { promisify } from "node:util";
20
22
  import { platform as platform$1 } from "node:process";
21
23
  import { fileURLToPath } from "node:url";
22
24
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
23
- import { createHash, createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
25
+ import { createHash, randomBytes } from "node:crypto";
24
26
  import { Resvg, initWasm } from "@resvg/resvg-wasm";
25
27
  import { createServer, request } from "node:http";
26
28
  import * as zlib from "node:zlib";
@@ -1897,2491 +1899,6 @@ async function runMsmAsync(root, args) {
1897
1899
  }
1898
1900
  }
1899
1901
  //#endregion
1900
- //#region src/trajectory/prompt.ts
1901
- /**
1902
- * trajectory/prompt.ts — **轨迹提示词**(Trajectory Prompt):一条轨迹"是谁、按什么章程工作"
1903
- *
1904
- * ## 概念(owner 2026-10-19 定向,逐字)
1905
- *
1906
- * > 「参考会话的系统提示词,我们建立**轨迹提示词**概念来代替人格。架构上会好很多」
1907
- *
1908
- * **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
1909
- * 三个关键词各是一条判据:
1910
- * - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
1911
- * - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `trajectory-constraint.json` 白名单与工具 schema)
1912
- * - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
1913
- *
1914
- * ## 为什么取代「人格」
1915
- *
1916
- * 「人格」把身份绑在"像不像一个人"上 ⇒ 因为叫人格,它被设计成**可以整体替换**
1917
- * (`persona` 置换 EAP 块,即 review F-05/R-14 那个悬置冲突)。而身份与章程**不该被替换,
1918
- * 只该被特化**。⇒ 本模块把"替换"改成"叠加":容器层与质量层永远保留,轨迹提示词只往上加。
1919
- *
1920
- * ## 落点(为什么在轨迹目录里)
1921
- *
1922
- * `AGENT_SESSIONS/<轨迹目录>/PROMPT.md`
1923
- *
1924
- * - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
1925
- * 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
1926
- * - **每条轨迹都能有**:不只是 轨迹约束。普通轨迹同样需要"我是谁、按什么章程"。
1927
- * - 与 `SESSION.md`(身体:延续/日志)、`trajectory-constraint.json`(约束面:能力)三件并列,
1928
- * 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
1929
- *
1930
- * ## 兼容(不静默坏)
1931
- *
1932
- * `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。轨迹约束的旧字段 `systemPromptFile`
1933
- * 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
1934
- */
1935
- /** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
1936
- const TRAJECTORY_PROMPT_FILENAME = "PROMPT.md";
1937
- /** 轨迹目录的 PROMPT.md 绝对路径(不论存在与否——调用方自己判存在性) */
1938
- function trajectoryPromptPath(root, dirName) {
1939
- return join(sessionsRoot(root), dirName, TRAJECTORY_PROMPT_FILENAME);
1940
- }
1941
- /**
1942
- * 该轨迹**有没有**轨迹提示词(单一判据)。
1943
- *
1944
- * 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,轨迹约束 旧字段 `systemPromptFile`
1945
- * 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-轨迹约束` 段)。
1946
- * 🔴 判据只有这一处:别在 轨迹约束那边自己拼路径再判(两处会漂)。
1947
- */
1948
- function hasTrajectoryPrompt(root, dirName) {
1949
- try {
1950
- return existsSync(trajectoryPromptPath(root, dirName));
1951
- } catch {
1952
- return false;
1953
- }
1954
- }
1955
- /**
1956
- * 轨迹提示词的**读取器**(长期热更新;改文件即生效,无需重启)。
1957
- *
1958
- * ## 为什么返回函数而不是字符串(R↓,实证代价)
1959
- *
1960
- * 旧实现把提示词**快照**进系统提示词段的闭包 ⇒ CCC 改了文件必须重启 dsh web 才生效
1961
- * (实测:改完 18KB 提示词,live agent 仍用旧文本)。现形态由系统提示词段的 `text()`
1962
- * **每轮调用**本读取器:按 **mtime + size** 缓存重读(命中缓存零 IO)。
1963
- *
1964
- * ## 三条失败语义(各治一个失败模式)
1965
- *
1966
- * - **文件不存在** ⇒ 返回空串(**不是错误**:普通轨迹没有章程是常态,且删掉即失效
1967
- * 必须是立即可见的——这就是"删掉 PROMPT.md 即恢复默认"的机制)
1968
- * - **读取失败** ⇒ 返回**上次成功内容**(⛔ 绝不返回空:空提示词会让轨迹失去章程),
1969
- * 并按去重标签告警一次
1970
- * - **首次就读不到** ⇒ 空串 + 告警一次(区分"本来没有"与"有但读不了")
1971
- *
1972
- * @param root CCC 根
1973
- * @param dirName 轨迹目录名(**不是编码**:目录名才是落盘位置)
1974
- */
1975
- function createTrajectoryPromptReader(root, dirName) {
1976
- const absPath = trajectoryPromptPath(root, dirName);
1977
- let cachedMtime = -1;
1978
- let cachedSize = -1;
1979
- let cachedText = "";
1980
- let warned = false;
1981
- return () => {
1982
- let st;
1983
- try {
1984
- st = statSync(absPath);
1985
- } catch (err) {
1986
- const code = err?.code;
1987
- cachedMtime = -1;
1988
- cachedSize = -1;
1989
- cachedText = "";
1990
- if (code !== "ENOENT" && !warned) {
1991
- warned = true;
1992
- console.warn(`[serenity-hooks] ✗ 轨迹提示词不可读(${absPath})——按"没有章程"处理: ${String(err?.message ?? err)}`);
1993
- }
1994
- return "";
1995
- }
1996
- try {
1997
- if (st.mtimeMs === cachedMtime && st.size === cachedSize) return cachedText;
1998
- const text = readUtf8(absPath).trim();
1999
- const isReload = cachedMtime !== -1;
2000
- cachedMtime = st.mtimeMs;
2001
- cachedSize = st.size;
2002
- cachedText = text;
2003
- warned = false;
2004
- if (isReload) console.info(`[serenity-hooks] ↻ 轨迹提示词已热重载(${TRAJECTORY_PROMPT_FILENAME},${text.length} 字符)`);
2005
- return text;
2006
- } catch (err) {
2007
- if (!warned) {
2008
- warned = true;
2009
- console.warn(`[serenity-hooks] ✗ 轨迹提示词读取失败(${absPath})——沿用上次成功内容: ${String(err?.message ?? err)}`);
2010
- }
2011
- return cachedText;
2012
- }
2013
- };
2014
- }
2015
- /**
2016
- * 装配用的**提示词段**(幂等:无内容 ⇒ 空串,宿主会丢弃空 section/空块)。
2017
- *
2018
- * 标记头 `=== Serenity Trajectory Prompt ===`:
2019
- * ① 让模型知道"下面是**这条轨迹自己的章程**",不是容器规则;
2020
- * ② 让诊断可以按块名 grep。
2021
- *
2022
- * 🔴 这段**追加**在容器层与质量层之后,**永不替换它们**——那正是本概念相对「人格」的核心修正。
2023
- */
2024
- function trajectoryPromptBlock(text) {
2025
- const body = text.trim();
2026
- if (body === "") return "";
2027
- return [
2028
- "",
2029
- "=== Serenity Trajectory Prompt ===",
2030
- body,
2031
- ""
2032
- ].join("\n");
2033
- }
2034
- //#endregion
2035
- //#region src/trajectory/carrier.ts
2036
- /**
2037
- * 等待 agent 空闲或销毁。
2038
- * @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
2039
- * @param agent 目标 agent
2040
- * @returns 空闲、agent 销毁、或会话销毁时结算(永不 reject)
2041
- */
2042
- function waitAgentIdle(ctx, agent) {
2043
- return new Promise((resolve) => {
2044
- let settled = false;
2045
- const disposers = [];
2046
- const finish = () => {
2047
- if (settled) return;
2048
- settled = true;
2049
- for (const dispose of disposers) try {
2050
- dispose();
2051
- } catch {}
2052
- resolve();
2053
- };
2054
- const subscribe = (name, cb) => {
2055
- try {
2056
- const off = ctx.on?.(name, cb);
2057
- if (typeof off === "function") disposers.push(off);
2058
- } catch {}
2059
- };
2060
- subscribe("agent/status", (payload) => {
2061
- const p = payload;
2062
- if (p?.agent === agent && p?.status === "idle") finish();
2063
- });
2064
- subscribe("agent/disposed", (payload) => {
2065
- if (payload?.agent === agent) finish();
2066
- });
2067
- const sessionId = agent.session?.id;
2068
- subscribe("session/disposed", (session) => {
2069
- const id = session?.id;
2070
- if (typeof id === "string" && id === sessionId) finish();
2071
- });
2072
- if (agent.status === "idle") finish();
2073
- });
2074
- }
2075
- //#endregion
2076
- //#region src/skills/discovery.ts
2077
- /**
2078
- * skills/discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
2079
- *
2080
- * 发现全部入口技能(原文全量,不截断)—— **现行三个来源**(原为四个,**来源 3 已于 2026-09-21 删除**):
2081
- * 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义)—— 最高优先
2082
- * 1. `.dsh/entry-skill` 指针文件(内容 = skill 名)—— 兼容旧约定
2083
- * 2. `.opencode/skills/*-serenity/SKILL.md` —— 自动扫描该 CCC 的顶层入口
2084
- * (命名模式 `*-serenity`)
2085
- * ~~3. `.dsh/skills/*-serenity/SKILL.md`(扫 ACC 装进 CCC 的技能副本)~~ —— 🔴 **已删**:
2086
- * owner 2026-09-21 令「连模板和安装命令一起删」(B 案)。**判据、实测证据与日期逐字记在
2087
- * `findEntrySkills()` 体内那一段注释里**——本头部只留结论,**不复述判据**(避免两处会漂的真相源)。
2088
- * 按名去重;顺序 = 记号文件 → 指针 → opencode 入口。
2089
- *
2090
- * 任何 CCC 都能自动注入其顶层入口 skill 全文(不硬编码名字)。
2091
- */
2092
- /** 顶层入口命名模式:目录名以 -serenity 结尾(home-serenity / tg-serenity / acc-serenity …) */
2093
- const SERENITY_SUFFIX = "-serenity";
2094
- function isSerenityEntry(name) {
2095
- return name.endsWith(SERENITY_SUFFIX);
2096
- }
2097
- /** 扫描某 skills 根下所有 `*-serenity` 目录(含 SKILL.md 的) */
2098
- function scanSerenityDirs(skillsRoot) {
2099
- if (!existsSync(skillsRoot)) return [];
2100
- return readdirSync(skillsRoot).filter((name) => isSerenityEntry(name)).map((name) => join(skillsRoot, name)).filter((dir) => statSync(dir).isDirectory() && existsSync(join(dir, "SKILL.md"))).sort();
2101
- }
2102
- /**
2103
- * 防穿越:skill 名必须是简单目录名(拒绝分隔符与 `..`/`.` 路径段)。
2104
- * v1.34.1:导出——轨迹 skill 声明(CCC 数据)的名字也要**先过这一关**才能用于拼路径
2105
- * (规格 `docs/cognitive-trajectory-skill-injection.md` §3「名字安全(必做)」)。
2106
- */
2107
- function isSafeSkillName(name) {
2108
- if (name.length === 0) return false;
2109
- if (name.includes("/") || name.includes("\\")) return false;
2110
- return !name.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
2111
- }
2112
- /**
2113
- * 按名在两个 skills 根下定位 SKILL.md(`.dsh/skills` 优先,其次 `.opencode/skills`)。
2114
- * v1.34.1:导出——轨迹 skill 注入复用本函数(不新造发现逻辑);不安全名在此**先行拒绝**
2115
- * (返回 null,且不发生任何以该名拼出的 fs 访问)。
2116
- */
2117
- function findSkillMd(root, name) {
2118
- if (!isSafeSkillName(name)) return null;
2119
- for (const base of [".dsh", ".opencode"]) {
2120
- const p = resolve(root, base, "skills", name, "SKILL.md");
2121
- if (existsSync(p)) return p;
2122
- }
2123
- return null;
2124
- }
2125
- /** 发现全部入口技能(原文全量,不截断) */
2126
- function findEntrySkills(root) {
2127
- const out = [];
2128
- const seen = /* @__PURE__ */ new Set();
2129
- const add = (path, source, forcedName) => {
2130
- if (!existsSync(path)) return;
2131
- const name = forcedName ?? basename(dirname(path));
2132
- if (seen.has(name)) return;
2133
- seen.add(name);
2134
- out.push({
2135
- name,
2136
- content: readFileSync(path, "utf-8"),
2137
- dir: dirname(path),
2138
- source
2139
- });
2140
- };
2141
- const marker = resolve(root, ".serenity");
2142
- if (existsSync(marker)) {
2143
- const name = readFileSync(marker, "utf-8").trim();
2144
- if (name) {
2145
- const md = findSkillMd(root, name);
2146
- if (md) add(md, "serenity-marker", name);
2147
- }
2148
- }
2149
- const pointer = resolve(root, ".dsh", "entry-skill");
2150
- if (existsSync(pointer)) {
2151
- const name = readFileSync(pointer, "utf-8").trim();
2152
- if (name) {
2153
- const md = findSkillMd(root, name);
2154
- if (md) add(md, "pointer", name);
2155
- }
2156
- }
2157
- for (const dir of scanSerenityDirs(resolve(root, ".opencode", "skills"))) add(join(dir, "SKILL.md"), "opencode");
2158
- return out;
2159
- }
2160
- function truncateContent(content, maxChars) {
2161
- if (content.length <= maxChars) return content;
2162
- return content.slice(0, maxChars) + `\n... (truncated, original length ${content.length} chars)`;
2163
- }
2164
- //#endregion
2165
- //#region src/skills/resource-index.ts
2166
- /**
2167
- * resource-index.ts — **两层披露树的第二层**:skill 参考件索引(纯逻辑,零 DSH 依赖)
2168
- *
2169
- * 设计真相源:`docs/serenity-skill-design.md` §4.2(六条硬性规则)+ §5.1(目录布局)。
2170
- * 一句话:**把 skill 的参考件"有什么"告诉模型,但不把"是什么"塞进去** ——
2171
- * 正文留给第三层(模型按需 `read`)。
2172
- *
2173
- * 为什么由我们生成(R↓,读宿主取证):宿主 `@deepseek-ai/dsh-skill` 的 `renderSkillContent()`
2174
- * (`lib/index.js` L57-81)**已经**产出 `<skill_resources>` 块,并逐字写着
2175
- * "Load referenced resources only as needed." ⇒ **两层模型是宿主自己给的**;而
2176
- * `SkillSummary` / `SkillDefinition` **没有任何列举子文件的位**(只有 `resourceBase` 一个基址)
2177
- * ⇒ 宿主刻意留给 provider 的正是**这一半:枚举**。
2178
- *
2179
- * 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
2180
- * ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
2181
- * ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
2182
- * 会让每请求的注入文本抖动;对照 `src/cro/core.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
2183
- * **同一件事两处的取舍相反,那个差异是判据不是巧合**);
2184
- * ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
2185
- * ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
2186
- * ⑤ **本体文件特殊标注** —— `skill.ttl` 存在 ⇒ 该行标 🧬 + 顶部加一句"已建模";
2187
- * ⑥ **范围 = `references/` 一级条目 + `skill.ttl`** —— 子目录**给名字 + 计数、不展开、不读正文**
2188
- * (🆕 2026-10-11 由**真实输入**逼出的修订:原写"不进子目录",实测 `references/` 下确实有内容目录,
2189
- * 只报"有 N 个子目录"不给名字 = **不可行动**;折中仍在裁决 Q2「只披露名字、不注入正文」的意图内)。
2190
- *
2191
- * 🔴 本文件**只认 `<skillDir>/skill.ttl`**(owner 裁决 Q6:静默、只认固定名)。
2192
- * `references/*-domain.ttl`(旧名)一律当**普通参考件**:不特殊对待、不告警 —— 那 14 份由 CCC 自行改名。
2193
- * 🔴 「未建模」不是异常(裁决 Q6 / 判据 J9):无 `skill.ttl` 的**普通 skill** 照常产出索引,
2194
- * **零告警、零误差** —— 增强必须对普通 skill 完全向后兼容。
2195
- *
2196
- * 🔴 **两个面,两个入口**(2026-10-12 增;owner 裁决 O2)—— 同源、但成本形态不同:
2197
- * · **载入面** `buildResourceIndex()` —— 完整索引(名字 + 首标题 + 字节),带 `.md` 文件头读取;
2198
- * 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`trajectory/skills.ts`
2199
- * order −45)/目录 provider 的 `get()`(`opencode-skills.ts`)。
2200
- * · **目录面** `buildResourceNameSuffix()` —— **只有名字**,**不读任何正文**;只挂一处:
2201
- * 目录 provider 的 `list()`(`available_skills` 的 `SkillSummary.description`)。
2202
- * 🔴 为什么目录面在这里:`SkillSummary` 契约里**没有列资源的位**(只有 name/description/whenToUse)
2203
- * ⇒ `description` 是唯一位置,且必须**自动生成**。
2204
- * ⇒ 两个面**共用** `collectEntries()`(范围 = 规则⑥只写一遍)。
2205
- */
2206
- /** 第二层占位口:本 skill 的本体约束文件(固定名,设计 §5.1) */
2207
- const SKILL_ONTOLOGY_FILE = "skill.ttl";
2208
- /** 参考件根(一级;不进子目录) */
2209
- const REFERENCE_DIR = "references";
2210
- /** 摘要长度上限(防单行超长标题把表格撑坏) */
2211
- const EXCERPT_MAX_CHARS = 60;
2212
- /** 读文件头部字节数上限:摘标题只值读这么些(不为一行标题读整个大文件) */
2213
- const HEAD_BYTES = 4096;
2214
- /** 目录计数上限:防病态大树把一次注入拖慢(超出 ⇒ 报 `≥N`,仍然响亮) */
2215
- const DIR_SCAN_MAX_FILES = 1e3;
2216
- /**
2217
- * 递归统计目录下的文件数与总字节(**只统计,不读内容**)。
2218
- * 为什么递归:设计原写"不进子目录",而**真实输入**显示 `references/` 下确有内容目录
2219
- * (`home-rhetoric/references/corpus-subject-own`/`family-profiles/references/baby-handlebook`/
2220
- * `home-serenity/references/diagram-lab`)⇒ 只报"有 N 个子目录"而**不给名字**是不可行动的。
2221
- * 折中:**给名字 + 计数,仍不展开、仍不读正文**(= 裁决 Q2「只披露名字、不注入正文」的意图内)。
2222
- */
2223
- function scanDirStats(dir) {
2224
- let files = 0;
2225
- let bytes = 0;
2226
- let capped = false;
2227
- const walk = (d) => {
2228
- if (capped || files >= DIR_SCAN_MAX_FILES) {
2229
- capped = true;
2230
- return;
2231
- }
2232
- let names = [];
2233
- try {
2234
- names = readdirSync(d);
2235
- } catch {
2236
- return;
2237
- }
2238
- for (const name of names) {
2239
- if (files >= DIR_SCAN_MAX_FILES) {
2240
- capped = true;
2241
- return;
2242
- }
2243
- const abs = join(d, name);
2244
- let isDir = false;
2245
- let size = 0;
2246
- try {
2247
- const st = statSync(abs);
2248
- isDir = st.isDirectory();
2249
- size = st.size;
2250
- } catch {
2251
- continue;
2252
- }
2253
- if (isDir) walk(abs);
2254
- else {
2255
- files++;
2256
- bytes += size;
2257
- }
2258
- }
2259
- };
2260
- walk(dir);
2261
- return {
2262
- files,
2263
- bytes,
2264
- capped
2265
- };
2266
- }
2267
- /** 按 UTF-8 **字节序**比较两个相对路径(规则②;不是 locale 比较、不是 mtime)。
2268
- * 参数放宽到 `{ key }`:**未加水**的条目与**已加水**的条目共用同一个比较器 ⇒ 排序规则只写一遍。 */
2269
- function byByteOrder(a, b) {
2270
- return Buffer.compare(a.key, b.key);
2271
- }
2272
- /**
2273
- * 千分位分组(规则②的延伸:**不依赖 locale**)。
2274
- * 为什么不用 `toLocaleString()`:它随宿主 locale 变 ⇒ 同一目录在不同机器上产出**不同的注入文本**
2275
- * (本仓已有一条「locale 稳健性」的挂账,不再添一处)。
2276
- */
2277
- function groupThousands(n) {
2278
- return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2279
- }
2280
- /**
2281
- * 取 markdown 首个 `#` 标题作摘要(规则④)。
2282
- *
2283
- * 细节(都由"可复现"推导):
2284
- * · **跳过 YAML frontmatter**(`---` 开头到下一 `---`)—— 否则题名行会被当成正文标题;
2285
- * · 只读文件**头部** `HEAD_BYTES`(一行标题不值得读整个大文件);
2286
- * · 剥掉行内记号(`*` `` ` `` `_` `#`)与首尾空白,压平内部连续空白;
2287
- * · 取不到 ⇒ `''`(**留空,不编** —— 编一个摘要等于让索引说假话);
2288
- * · 超长截断时**留省略号**(与规则③同一精神:截断要看得见)。
2289
- */
2290
- function firstHeading(file) {
2291
- let head;
2292
- try {
2293
- head = readFileSync(file, "utf-8").slice(0, HEAD_BYTES);
2294
- } catch {
2295
- return "";
2296
- }
2297
- const lines = head.split(/\r?\n/);
2298
- let start = 0;
2299
- if (lines[0]?.trim() === "---") {
2300
- for (let i = 1; i < lines.length; i++) if (lines[i]?.trim() === "---") {
2301
- start = i + 1;
2302
- break;
2303
- }
2304
- }
2305
- for (let i = start; i < lines.length; i++) {
2306
- const m = /^#{1,6}\s+(.*)$/.exec(lines[i] ?? "");
2307
- if (!m) continue;
2308
- const clean = (m[1] ?? "").replace(/[*`_]/g, "").replace(/\s+/g, " ").trim();
2309
- if (clean === "") continue;
2310
- return clean.length > EXCERPT_MAX_CHARS ? `${clean.slice(0, 59)}…` : clean;
2311
- }
2312
- return "";
2313
- }
2314
- /**
2315
- * 收集第二层的**条目**(范围 = 规则⑥:`references/` 一级条目 + 固定名本体)。
2316
- *
2317
- * 🔴 **只做 `readdir` / `stat`,不读任何文件正文** —— 两个消费者(第二层索引 / 目录面的名字清单)
2318
- * **共用这一份范围定义**:规则⑥只该有一个家,各写一遍必然漂移。
2319
- * 🔴 返回已按**字节序**排好(规则②:确定性,不用 mtime)。
2320
- */
2321
- function collectEntries(skillDir) {
2322
- const entries = [];
2323
- const refDir = join(skillDir, REFERENCE_DIR);
2324
- if (existsSync(refDir)) {
2325
- let names = [];
2326
- try {
2327
- names = readdirSync(refDir).sort();
2328
- } catch {
2329
- names = [];
2330
- }
2331
- for (const name of names) {
2332
- const abs = join(refDir, name);
2333
- let isDir = false;
2334
- try {
2335
- isDir = statSync(abs).isDirectory();
2336
- } catch {
2337
- continue;
2338
- }
2339
- const rel = `${REFERENCE_DIR}/${name}`;
2340
- entries.push({
2341
- rel,
2342
- kind: isDir ? "dir" : "file",
2343
- abs,
2344
- ontology: false,
2345
- key: Buffer.from(rel, "utf-8")
2346
- });
2347
- }
2348
- }
2349
- const ontoAbs = join(skillDir, SKILL_ONTOLOGY_FILE);
2350
- if (existsSync(ontoAbs)) entries.push({
2351
- rel: SKILL_ONTOLOGY_FILE,
2352
- kind: "file",
2353
- abs: ontoAbs,
2354
- ontology: true,
2355
- key: Buffer.from(SKILL_ONTOLOGY_FILE, "utf-8")
2356
- });
2357
- entries.sort(byByteOrder);
2358
- return entries;
2359
- }
2360
- /**
2361
- * 给条目**加水**(字节 + 摘要)—— 这两项都要读盘,因此与"有哪些条目"分开。
2362
- * 读不到 ⇒ `null`(宁可少列,也不列出读不到的项)。
2363
- */
2364
- function hydrate(entry) {
2365
- if (entry.kind === "dir") {
2366
- const { files, bytes, capped } = scanDirStats(entry.abs);
2367
- return {
2368
- rel: entry.rel,
2369
- kind: "dir",
2370
- bytes,
2371
- excerpt: `子目录 · ${capped ? `≥${groupThousands(files)}` : groupThousands(files)} 个文件`,
2372
- ontology: false,
2373
- key: entry.key
2374
- };
2375
- }
2376
- let bytes;
2377
- try {
2378
- bytes = statSync(entry.abs).size;
2379
- } catch {
2380
- return null;
2381
- }
2382
- return {
2383
- rel: entry.rel,
2384
- kind: "file",
2385
- bytes,
2386
- excerpt: entry.rel.endsWith(".md") ? firstHeading(entry.abs) : "",
2387
- ontology: entry.ontology,
2388
- key: entry.key
2389
- };
2390
- }
2391
- /** 收集候选(`references/` 一级条目 + 固定名本体),加好水并按字节序排好 */
2392
- function collectItems(skillDir) {
2393
- const items = [];
2394
- for (const entry of collectEntries(skillDir)) {
2395
- const item = hydrate(entry);
2396
- if (item !== null) items.push(item);
2397
- }
2398
- return items;
2399
- }
2400
- /**
2401
- * 生成第二层索引正文。
2402
- *
2403
- * @param skillDir skill 目录绝对路径
2404
- * @param options 上限覆盖(缺省用本模块常量)
2405
- * @returns 索引正文;**无可列项 ⇒ `''`**(规则⑥的一般化:`references/` 与 `skill.ttl` 都没有
2406
- * ⇒ 整块不产出 —— 空标题是噪声)。调用方按"空串不产出该块"处理。
2407
- */
2408
- function buildResourceIndex(skillDir, options = {}) {
2409
- const maxItems = options.maxItems ?? 40;
2410
- const maxChars = options.maxChars ?? 4096;
2411
- const items = collectItems(skillDir);
2412
- if (items.length === 0) return "";
2413
- const modeled = items.some((i) => i.ontology);
2414
- const shown = items.slice(0, maxItems);
2415
- const hidden = items.length - shown.length;
2416
- const lines = ["### 本 skill 的参考件(第二层 · 按需读取)", "> 这里只有**清单**,没有正文:需要时用 `read` 打开下列路径(相对本 skill 目录)。"];
2417
- if (modeled) lines.push("> 🧬 **本 skill 已建模**:文字内容以 `skill.ttl` 为总约束。");
2418
- lines.push("", "| 文件 | 摘要 | 字节 |", "|---|---|---|");
2419
- for (const item of shown) {
2420
- const label = item.kind === "dir" ? `${item.rel}/` : item.rel;
2421
- const excerpt = item.ontology ? "🧬 本 skill 的本体约束(已建模)" : item.excerpt;
2422
- lines.push(`| ${label} | ${excerpt} | ${groupThousands(item.bytes)} |`);
2423
- }
2424
- if (hidden > 0) lines.push("", `…**还有 ${hidden} 项未列出**(本层单次上限 ${maxItems} 项)。`);
2425
- const body = lines.join("\n");
2426
- if (body.length <= maxChars) return body;
2427
- return `${body.slice(0, maxChars)}\n…(第二层索引已按 ${groupThousands(maxChars)} 字符上限截断,原文 ${groupThousands(body.length)} 字符)`;
2428
- }
2429
- /**
2430
- * 收集**只有名字**的第二层清单(目录面专用)。
2431
- *
2432
- * 为什么单独一个入口(而不是复用 `buildResourceIndex`):**成本形态不同** ——
2433
- * 索引要给每个 `.md` 读文件头取摘要(规则④),而目录面是**每请求 × 每个 skill** 都要算的
2434
- * ⇒ 这条路径**一个字节正文都不读**(只 `readdir`/`stat`)。
2435
- * 范围仍与索引**同源**(共用 `collectEntries` ⇒ 规则⑥只有一个家)。
2436
- */
2437
- function buildResourceNames(skillDir, options = {}) {
2438
- const maxItems = options.maxItems ?? 40;
2439
- const entries = collectEntries(skillDir);
2440
- const shown = entries.slice(0, maxItems);
2441
- return {
2442
- names: shown.map((e) => e.kind === "dir" ? `${basename(e.rel)}/` : basename(e.rel)),
2443
- hidden: entries.length - shown.length
2444
- };
2445
- }
2446
- /**
2447
- * 目录面后缀:把名字清单拼成**可直接接在 `description` 后面**的一段文本。
2448
- *
2449
- * 🔴 **自动生成,绝不手写**(手写必与文件树漂);无可列项 ⇒ `''`(**普通 skill 零噪声** —— 判据 J9)。
2450
- * 🔴 **有界 + 响亮**(规则③):超上限时写出"另有 N 项",**绝不静默截断**。
2451
- */
2452
- function buildResourceNameSuffix(skillDir, options = {}) {
2453
- const { names, hidden } = buildResourceNames(skillDir, options);
2454
- if (names.length === 0) return "";
2455
- const tail = hidden > 0 ? ` …另有 ${hidden} 项` : "";
2456
- return `(参考件:${names.join(" / ")}${tail})`;
2457
- }
2458
- //#endregion
2459
- //#region src/trajectory/skills.ts
2460
- /**
2461
- * trajectory/skills.ts — 轨迹 skill 声明解析 + 注入内容装配(纯逻辑,零 DSH 依赖)
2462
- *
2463
- * 接口真相源:`docs/cognitive-trajectory-skill-injection.md` v1.0(§2 数据契约 / §3 注入契约)。
2464
- * 一句话:**轨迹自己声明它要挂哪些 skill**(`SESSION.md` 顶部 YAML frontmatter 的 `skills:`),
2465
- * ACC 在**绑定期间**把这些 skill 的 `SKILL.md` 全文注入系统提示词。
2466
- *
2467
- * 分工(R↓:为什么解析与注册分开):
2468
- * · 本模块 = **纯文本层**(解析 / 装配 / 安全判定)——可脱离 DSH 单测;
2469
- * · `seams/system-prompt.ts` 的 `registerCognitiveTrajectorySkillSection` = **宿主接入层**
2470
- * (per-agent `ctx.systemPrompt.section`),只做门与注册,不再解析文本。
2471
- *
2472
- * 边界(规格 §2.2):**不解析** `skills` 以外任何键的语义(CCC 可自由使用,ACC 视而不见);
2473
- * **不改写** frontmatter(ACC 只读)。
2474
- */
2475
- /** 全文注入总长上限(字符数;规格 §3「缺省 32 KB」——防上下文爆炸) */
2476
- const COGNITIVE_TRAJECTORY_SKILLS_MAX_CHARS = 32768;
2477
- /** 缺失标记(规格 §3/§6-3 的验收锚点:任何"拿不到全文"的情况都必须带它,绝不静默)
2478
- * 🔴 2026-09-25(S142 分批清死代码·第 1 批):**降为模块内常量**——全仓唯一消费者在本文件,
2479
- * 导出只服务测试(测试用的是字面量 `'[缺失]'`)⇒ 导出面无意义。 */
2480
- const SKILL_MISSING_MARK = "[缺失]";
2481
- /** 统一缺失提示(规格 §3 逐字约定:`[缺失] <name>(未找到该 skill)`) */
2482
- function missingNotice(name) {
2483
- return `${SKILL_MISSING_MARK} ${name}(未找到该 skill)`;
2484
- }
2485
- /**
2486
- * 抽取文件顶部 YAML frontmatter 区(规格 §2.1)。
2487
- *
2488
- * 规则:**第一行必须是 `---`**;到下一个 `---` 行为止是 frontmatter 区。
2489
- * 首行不是 `---`,或没有闭合 `---` ⇒ 视为**无 frontmatter**(返回 null)。
2490
- * 边界判断(R↓):未闭合块按"无 frontmatter"处理而非吞掉整个文件——半个声明块是格式错误,
2491
- * 猜它的意图等于把坏数据当好消息(宁可零注入,也不注入来路不明的半截列表)。
2492
- *
2493
- * @param content SESSION.md 全文
2494
- * @returns frontmatter 区各行(不含两条 `---` 定界行);无 → null
2495
- */
2496
- function extractFrontmatter(content) {
2497
- const lines = content.split(/\r?\n/);
2498
- if (lines[0] !== "---") return null;
2499
- for (let i = 1; i < lines.length; i++) if (lines[i] === "---") return lines.slice(1, i);
2500
- return null;
2501
- }
2502
- /** 行内数组值 `[a, b, c]`(仅当首尾为方括号时视为行内数组;否则返回 null) */
2503
- function parseInlineArray(value) {
2504
- const trimmed = value.trim();
2505
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
2506
- return trimmed.slice(1, -1).split(",").map((s) => s.trim()).filter((s) => s !== "");
2507
- }
2508
- /**
2509
- * 解析声明(规格 §2.1,**最小实现,不引 YAML 依赖**)。
2510
- *
2511
- * 只认 `skills:` 键;未知键一律忽略(向前兼容,不报错)。两种值形态都支持:
2512
- * ① 行内数组 `skills: [home-rhetoric, acc-eap]`
2513
- * ② 缩进列表(`skills:` 后各起一行 ` - a`)
2514
- * 名字:去首尾空白、空项丢弃、**保持书写顺序**(注入顺序 = 书写顺序)、同名去重(保留首次位置)。
2515
- *
2516
- * 未定义形态(不在规格的两种值形态内,如 `skills: a` 裸标量)⇒ **视为无声明**:
2517
- * 本函数只认契约里写明的两种形态,猜第三种等于自造契约(规格 §2.1 只列了 ①②)。
2518
- *
2519
- * @param sessionMd SESSION.md 全文
2520
- * @returns 声明的 skill 名(按书写顺序去重);无 frontmatter / 无 `skills` 键 / 值为空 → `[]`
2521
- */
2522
- function parseDeclaredSkills(sessionMd) {
2523
- const fm = extractFrontmatter(sessionMd);
2524
- if (!fm) return [];
2525
- const out = [];
2526
- const seen = /* @__PURE__ */ new Set();
2527
- const push = (name) => {
2528
- const clean = name.trim();
2529
- if (clean === "" || seen.has(clean)) return;
2530
- seen.add(clean);
2531
- out.push(clean);
2532
- };
2533
- for (let i = 0; i < fm.length; i++) {
2534
- const line = fm[i] ?? "";
2535
- const m = /^skills\s*:(.*)$/.exec(line);
2536
- if (!m) continue;
2537
- const rest = (m[1] ?? "").trim();
2538
- const inline = parseInlineArray(rest);
2539
- if (inline) {
2540
- for (const name of inline) push(name);
2541
- continue;
2542
- }
2543
- if (rest !== "") continue;
2544
- for (let j = i + 1; j < fm.length; j++) {
2545
- const item = /^\s*-\s*(.*)$/.exec(fm[j] ?? "");
2546
- if (!item) break;
2547
- push(item[1] ?? "");
2548
- }
2549
- }
2550
- return out;
2551
- }
2552
- /** SESSION.md 绝对路径(绑定记录存的是绝对路径;非绝对态按 CCC 根解析——防御旧数据) */
2553
- function absoluteMdPath(root, mdPath) {
2554
- return mdPath.startsWith(root) ? mdPath : resolve(root, mdPath);
2555
- }
2556
- /**
2557
- * 注册门(规格 §3「无绑定 / 无声明 ⇒ 完全不注册该 section」)。
2558
- *
2559
- * 返回 true 的两种情况:
2560
- * · 文件在且**有非空声明** → 注册(有内容可注入)
2561
- * · 文件**不在 / 读不到** → 同样注册——此时 section 文本是响亮的 `[缺失]` 提示
2562
- * (规格 §6-6 要求"不静默";不注册就等于静默)
2563
- * 其余(文件在但无 frontmatter / 无 `skills` 键 / 值为空)⇒ false,不注册、不产生噪声。
2564
- */
2565
- function hasCognitiveTrajectorySkillsDeclaration(root, mdPath) {
2566
- const abs = absoluteMdPath(root, mdPath);
2567
- if (!existsSync(abs)) return true;
2568
- try {
2569
- return parseDeclaredSkills(readFileSync(abs, "utf-8")).length > 0;
2570
- } catch {
2571
- return true;
2572
- }
2573
- }
2574
- /** 去空白 + 丢空项 + 同名去重(保留首次出现位置)——两条声明来源共用同一规范化 */
2575
- function normalizeNames(names) {
2576
- const out = [];
2577
- const seen = /* @__PURE__ */ new Set();
2578
- for (const raw of names) {
2579
- const clean = raw.trim();
2580
- if (clean === "" || seen.has(clean)) continue;
2581
- seen.add(clean);
2582
- out.push(clean);
2583
- }
2584
- return out;
2585
- }
2586
- /**
2587
- * 合并两条声明来源(v1.44.0):**CCC 级在前,轨迹级追加**,同名去重取首次位置。
2588
- *
2589
- * 两条来源(owner 令 2026-09-20):
2590
- * · **CCC 级** = `.opencode/serenity.json` 的 `cognitiveTrajectory.skills`(本 CCC 所有轨迹共有的底座);
2591
- * · **轨迹级** = 该轨迹 `SESSION.md` frontmatter 的 `skills:`(这条轨迹额外的)。
2592
- *
2593
- * 为什么是**并集**而不是二选一(R↓):C1 设计当初否决"纯 CCC 全局"的理由是"会**丢掉
2594
- * per-trajectory**";并集把两侧都保住——容器给底座,单条轨迹仍可加自己的额外项。
2595
- * 为什么 CCC 级**在前**:底座先立、增量随后,与"入口身份块 order=-50 → 本 section order=-45"
2596
- * 的既有先后观感一致。
2597
- */
2598
- function mergeSkillNames(cccSkills, trajSkills) {
2599
- return normalizeNames([...cccSkills, ...trajSkills]);
2600
- }
2601
- /**
2602
- * 装配注入正文(规格 §3):按**合并后的**顺序拼接各 skill 的 `SKILL.md` 全文,每段带来源抬头
2603
- * `=== skill: <name> ===`;拿不到全文的一律以 `[缺失]` 响亮提示占位(**不静默**)。
2604
- *
2605
- * 安全(规格 §3 🔒):名字来自 CCC 数据(frontmatter / CCC 配置)⇒ **先过 `isSafeSkillName`**;
2606
- * 不安全 ⇒ 按缺失处理,且不调用 `findSkillMd` ⇒ **不发生任何以该名拼出的 fs 访问**。
2607
- *
2608
- * 长度守卫:复用已导出的 `truncateContent`,**在合并之后**统一截断(两条来源共享同一上限,
2609
- * 不是各给 32 KB——否则上限会随来源数翻倍)。
2610
- *
2611
- * @param root CCC 根
2612
- * @param mdPath 轨迹的 SESSION.md 路径(**来自绑定的 `mdPath`**,不用 label 拼)
2613
- * @param maxChars 总长上限(字符)
2614
- * @param cccSkills CCC 级声明(`readCognitiveTrajectorySkills`;缺省空 = 只有轨迹级,行为与 v1.43 一致)
2615
- * @param onInjectedNames 观测回调(本次请求实际带上的 skill 名;**纯观测**,供用量统计用——
2616
- * 回调抛错被吞,**不影响注入正文**)
2617
- * @returns 注入正文;两条来源皆无声明 → `''`(空串 = 宿主不产出该 section 块);
2618
- * SESSION.md 缺失/读失败 → 响亮提示(若同时有 CCC 级声明,则提示在前、正文随后——
2619
- * **提示不因"别处有内容"而被吞掉**)
2620
- */
2621
- function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIVE_TRAJECTORY_SKILLS_MAX_CHARS, cccSkills = [], onInjectedNames) {
2622
- const abs = absoluteMdPath(root, mdPath);
2623
- let trajNames = [];
2624
- let notice = "";
2625
- if (!existsSync(abs)) notice = `${SKILL_MISSING_MARK} ${abs}(轨迹的 SESSION.md 不存在——skill 声明无从读取;若刚重建/移动过轨迹,请确认绑定记录)`;
2626
- else try {
2627
- trajNames = parseDeclaredSkills(readFileSync(abs, "utf-8"));
2628
- } catch (err) {
2629
- notice = `${SKILL_MISSING_MARK} ${abs}(SESSION.md 读取失败:${err instanceof Error ? err.message : String(err)})`;
2630
- }
2631
- const names = mergeSkillNames(cccSkills, trajNames);
2632
- if (names.length === 0) return notice;
2633
- if (onInjectedNames) try {
2634
- onInjectedNames(names);
2635
- } catch {}
2636
- const blocks = [];
2637
- for (const name of names) {
2638
- if (!isSafeSkillName(name)) {
2639
- blocks.push(`${SKILL_MISSING_MARK} ${name}(名字不安全,已拒绝解析路径)`);
2640
- continue;
2641
- }
2642
- const file = findSkillMd(root, name);
2643
- if (!file) {
2644
- blocks.push(missingNotice(name));
2645
- continue;
2646
- }
2647
- try {
2648
- const index = buildResourceIndex(dirname(file));
2649
- const body = readFileSync(file, "utf-8");
2650
- blocks.push(`=== skill: ${name} ===\n${index ? `${body}\n\n${index}` : body}`);
2651
- } catch {
2652
- blocks.push(missingNotice(name));
2653
- }
2654
- }
2655
- const body = blocks.join("\n\n");
2656
- return truncateContent(notice ? `${notice}\n\n${body}` : body, maxChars);
2657
- }
2658
- //#endregion
2659
- //#region src/surface/usage.ts
2660
- /**
2661
- * surface/usage.ts — ACC 用量统计(**skill 加载次数** + **MSM 执行次数**;落该 CCC 的 `_tmp`)
2662
- *
2663
- * ## 所有者令(本模块的存在理由)
2664
- *
2665
- * 「我需要对 ACC 增加一个统计功能,包括 **skill 加载次数统计,msm 执行次数统计**;
2666
- * 要求这个统计**自动生成在 CCC 的 `_tmp` 内的指定文件**」
2667
- * +(对齐时确认)「**skill 加载**两个口径**都记、用字段区分**」「**只记名字、不记参数**」
2668
- * 「**重启后读旧文件续算**」。
2669
- *
2670
- * ## 它解决什么麻烦(白话)
2671
- *
2672
- * 在此之前,「**哪些 skill 真的被加载过、哪些 MSM 真的在跑**」在 ACC 侧**一个数都没有** ——
2673
- * 要判断「这东西还有人用吗」,只能靠翻会话记录**猜**。
2674
- * ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
2675
- * 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
2676
- *
2677
- * ## 🔴 铁律(与 `cro/log.ts` 同族)
2678
- *
2679
- * **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
2680
- * 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
2681
- * 调用点在 `tools/post-execute` 的中途 ⇒ **更不能抛**(否则等于用统计打断会话)。
2682
- *
2683
- * ## 🔴 只记名字(所有者令;也是本模块唯一的"不入盘"约束)
2684
- *
2685
- * **绝不写 `arguments`**:MSM 参数里可能有凭据 / 路径 / 正文。
2686
- * ⇒ 本模块只保留**计数键**(skill 名 / MSM 名),并对其做**长度与数量上限**(见常量),
2687
- * 使文件规模**结构上有界**(不需要轮转,也不会被异常名字撑爆)。
2688
- *
2689
- * ## 🔴 重启后必须续算(所有者令)
2690
- *
2691
- * 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
2692
- * 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
2693
- * `cro/log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
2694
- *
2695
- * ## 口径(两个字段,勿合并)
2696
- *
2697
- * | 字段 | 含义 | 喂它的地方 |
2698
- * |---|---|---|
2699
- * | `skill.loads` | 模型**主动**调宿主 `skill` 工具(把某 skill 的 `SKILL.md` 全文载入) | `tools/post-execute`(工具名 = `skill`) |
2700
- * | `skill.injections` | ACC **每请求注入**的 skill 段(`cognitiveTrajectory.skills` / 轨迹 frontmatter 声明) | `seams/system-prompt.ts` 的 cognitive-trajectory-skills section 求值回调 |
2701
- * | `msm` | `msm` 工具的一次执行,按 **MSM 名**分桶 | `tools/post-execute`(工具名 = `msm`) |
2702
- *
2703
- * 🔴 **两者差好几个数量级**:`loads` 是模型的**动作**(稀疏),`injections` 是**请求的属性**(稠密)。
2704
- * 合并成一个数就**再也读不出**「这个 skill 是被人主动用的,还是只是被配上了」。
2705
- */
2706
- /** 落点目录(所有者令:CCC 的 `_tmp` 内) */
2707
- const USAGE_STATS_SUBDIR = "_tmp";
2708
- /** 固定文件名(所有者令「指定文件」;对齐时我取的名字,owner 未反对 ⇒ 定为常量) */
2709
- const USAGE_STATS_FILENAME = "acc-usage.json";
2710
- /** 每个桶的键数上限(**结构上界**,见文件头"只记名字"节) */
2711
- const USAGE_MAX_KEYS_PER_BUCKET = 1e3;
2712
- const USAGE_STATS_VERSION = 1;
2713
- /** 统计文件绝对路径(**纯函数**) */
2714
- function usageStatsPath(root) {
2715
- return join(root, USAGE_STATS_SUBDIR, USAGE_STATS_FILENAME);
2716
- }
2717
- /** 空账本(**纯函数**) */
2718
- function emptyUsageStats() {
2719
- return {
2720
- version: USAGE_STATS_VERSION,
2721
- updatedAt: isoLocal(0),
2722
- skill: {
2723
- loads: {},
2724
- injections: {}
2725
- },
2726
- msm: {}
2727
- };
2728
- }
2729
- /** 单条计数形状校验(容忍手改:字段缺失/类型不符 ⇒ 丢弃该条,不阻断整档) */
2730
- function asCounter(value) {
2731
- const c = value;
2732
- if (!c || typeof c !== "object") return null;
2733
- if (typeof c.count !== "number" || !Number.isFinite(c.count) || c.count < 0) return null;
2734
- const firstAt = typeof c.firstAt === "string" ? c.firstAt : "";
2735
- const lastAt = typeof c.lastAt === "string" ? c.lastAt : firstAt;
2736
- return {
2737
- count: Math.floor(c.count),
2738
- firstAt,
2739
- lastAt
2740
- };
2741
- }
2742
- /** 桶校验(非对象 ⇒ 空桶;逐键过滤非法条) */
2743
- function asBucket(value) {
2744
- const out = {};
2745
- if (!value || typeof value !== "object") return out;
2746
- for (const [k, v] of Object.entries(value)) {
2747
- const c = asCounter(v);
2748
- if (c) out[k] = c;
2749
- }
2750
- return out;
2751
- }
2752
- /**
2753
- * 读取账本(**永不抛**;读坏 ⇒ 空账本 + 返回 error 文本,**不静默吞掉**)。
2754
- * 文件不存在 = 正常初态(`error: null` 且空账本)。
2755
- */
2756
- function loadUsageStats(root) {
2757
- const path = usageStatsPath(root);
2758
- if (!existsSync(path)) return {
2759
- stats: emptyUsageStats(),
2760
- error: null
2761
- };
2762
- try {
2763
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
2764
- const skill = parsed.skill ?? {};
2765
- return {
2766
- stats: {
2767
- version: USAGE_STATS_VERSION,
2768
- updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : isoLocal(0),
2769
- skill: {
2770
- loads: asBucket(skill.loads),
2771
- injections: asBucket(skill.injections)
2772
- },
2773
- msm: asBucket(parsed.msm)
2774
- },
2775
- error: null
2776
- };
2777
- } catch (err) {
2778
- return {
2779
- stats: emptyUsageStats(),
2780
- error: `用量统计解析失败(${String(err?.message ?? err)})`
2781
- };
2782
- }
2783
- }
2784
- /** 键是否可记(非空 + 不超长;**不截断**——截断会造出一个不存在的名字) */
2785
- function isRecordableUsageName(name) {
2786
- return typeof name === "string" && name !== "" && [...name].length <= 120;
2787
- }
2788
- /**
2789
- * 在桶上自增一格(**纯函数**:不改入参,返回新桶)。
2790
- * 新键超桶上限 ⇒ **不新增**(但仍自增已有键)⇒ 键数**结构上有界**。
2791
- */
2792
- function bumpBucket(bucket, name, at, maxKeys = USAGE_MAX_KEYS_PER_BUCKET) {
2793
- const prev = bucket[name];
2794
- if (!prev && Object.keys(bucket).length >= maxKeys) return bucket;
2795
- const next = prev ? {
2796
- count: prev.count + 1,
2797
- firstAt: prev.firstAt || at,
2798
- lastAt: at
2799
- } : {
2800
- count: 1,
2801
- firstAt: at,
2802
- lastAt: at
2803
- };
2804
- return {
2805
- ...bucket,
2806
- [name]: next
2807
- };
2808
- }
2809
- /** 从工具入参里取名字(首个命中的键胜出;取不到 ⇒ `null`) */
2810
- function nameFromArgs(args, keys) {
2811
- if (args === null || typeof args !== "object") return null;
2812
- const a = args;
2813
- for (const k of keys) if (isRecordableUsageName(a[k])) return a[k];
2814
- return null;
2815
- }
2816
- /** `msm` 工具的名字键(`msm(name, args)` 的第一个参数) */
2817
- const MSM_NAME_KEYS = ["name"];
2818
- /** `skill` 工具的名字键(宿主 `skill` 工具用 `name`;另两个是防御性容错) */
2819
- const SKILL_NAME_KEYS = [
2820
- "name",
2821
- "skill",
2822
- "skillName"
2823
- ];
2824
- const NOTHING = {
2825
- ok: true,
2826
- error: null,
2827
- recorded: null,
2828
- name: null,
2829
- wrote: false
2830
- };
2831
- /**
2832
- * 读 → 自增 → 原子写(**本模块唯一写盘通道;永不抛**)。
2833
- *
2834
- * @param root CCC 根
2835
- * @param mutate 在已载入的账本上做一次自增(返回值 = 本次记录的结论;返回 `NOTHING` 则不写盘)
2836
- * @param nowMs 当前毫秒(可注入 ⇒ 便于测 firstAt/lastAt)
2837
- */
2838
- function writeThrough(root, mutate, nowMs = Date.now()) {
2839
- const path = usageStatsPath(root);
2840
- try {
2841
- const at = isoLocal(nowMs);
2842
- const loaded = loadUsageStats(root);
2843
- const outcome = mutate(loaded.stats, at);
2844
- if (outcome.recorded === null) return outcome;
2845
- loaded.stats.updatedAt = at;
2846
- mkdirSync(join(root, USAGE_STATS_SUBDIR), { recursive: true });
2847
- const tmp = `${path}.tmp`;
2848
- writeFileSync(tmp, `${JSON.stringify(loaded.stats, null, 2)}\n`, "utf-8");
2849
- renameSync(tmp, path);
2850
- return {
2851
- ...outcome,
2852
- ok: true,
2853
- error: null,
2854
- wrote: true
2855
- };
2856
- } catch (err) {
2857
- return {
2858
- ok: false,
2859
- error: `用量统计写入失败(${String(err?.message ?? err)})`,
2860
- recorded: null,
2861
- name: null,
2862
- wrote: false
2863
- };
2864
- }
2865
- }
2866
- /**
2867
- * 记录一次工具调用的用量(**`tools/post-execute` 的喂入口**)。
2868
- *
2869
- * 只认两类工具名;其余一律 `NOTHING`(**不产生文件、不产生噪声**)。
2870
- * 🔴 **不看 `arguments` 的其余内容**——只取名字键。
2871
- */
2872
- function recordToolUsage(root, toolName, args, nowMs = Date.now()) {
2873
- if (toolName === "msm") {
2874
- const name = nameFromArgs(args, MSM_NAME_KEYS);
2875
- if (!name) return NOTHING;
2876
- return writeThrough(root, (stats, at) => {
2877
- stats.msm = bumpBucket(stats.msm, name, at);
2878
- return {
2879
- ok: true,
2880
- error: null,
2881
- recorded: "msm",
2882
- name,
2883
- wrote: false
2884
- };
2885
- }, nowMs);
2886
- }
2887
- if (toolName === "skill") {
2888
- const name = nameFromArgs(args, SKILL_NAME_KEYS);
2889
- if (!name) return NOTHING;
2890
- return writeThrough(root, (stats, at) => {
2891
- stats.skill.loads = bumpBucket(stats.skill.loads, name, at);
2892
- return {
2893
- ok: true,
2894
- error: null,
2895
- recorded: "skill.load",
2896
- name,
2897
- wrote: false
2898
- };
2899
- }, nowMs);
2900
- }
2901
- return NOTHING;
2902
- }
2903
- /**
2904
- * 记录一次 **skill 注入**(每个请求的 skill 段装配时调用)。
2905
- *
2906
- * ⚠️ 与 `recordToolUsage` 不同,本函数的调用频度 = **请求频度**(稠密):
2907
- * 调用方应**只在真的注入了非空内容时**调用(空 section 由宿主丢弃 ⇒ 不产生噪声)。
2908
- * 一次调用只写一次盘(`names` 内的多个 skill 合并在**同一次**读改写里)。
2909
- */
2910
- function recordSkillInjections(root, names, nowMs = Date.now()) {
2911
- const usable = names.filter((n) => isRecordableUsageName(n));
2912
- if (usable.length === 0) return NOTHING;
2913
- return writeThrough(root, (stats, at) => {
2914
- let bucket = stats.skill.injections;
2915
- for (const n of usable) bucket = bumpBucket(bucket, n, at);
2916
- stats.skill.injections = bucket;
2917
- return {
2918
- ok: true,
2919
- error: null,
2920
- recorded: "skill.injection",
2921
- name: usable.join(","),
2922
- wrote: false
2923
- };
2924
- }, nowMs);
2925
- }
2926
- /** 验证窗口:当前时步 ± 1(容忍时钟漂移/生成延迟) */
2927
- const TOTP_WINDOW = 1;
2928
- /** 输出位数(标准 6 位) */
2929
- const TOTP_DIGITS = 6;
2930
- const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2931
- /** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
2932
- function base32Decode(input) {
2933
- const clean = input.toUpperCase().replace(/[\s=]/g, "");
2934
- if (clean.length === 0) throw new Error("empty base32");
2935
- const out = [];
2936
- let bits = 0;
2937
- let value = 0;
2938
- for (const ch of clean) {
2939
- const idx = B32_ALPHABET.indexOf(ch);
2940
- if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
2941
- value = value << 5 | idx;
2942
- bits += 5;
2943
- if (bits >= 8) {
2944
- out.push(value >>> bits - 8 & 255);
2945
- bits -= 8;
2946
- }
2947
- }
2948
- return Uint8Array.from(out);
2949
- }
2950
- /** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
2951
- function totpCode(secretBase32, counter) {
2952
- const key = base32Decode(secretBase32);
2953
- const msg = Buffer.alloc(8);
2954
- msg.writeBigUInt64BE(BigInt(counter));
2955
- const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
2956
- const offset = digest[digest.length - 1] & 15;
2957
- const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
2958
- return String(bin % 10 ** TOTP_DIGITS).padStart(TOTP_DIGITS, "0");
2959
- }
2960
- /** 当前 epoch 秒 */
2961
- function nowEpochSeconds() {
2962
- return Math.floor(Date.now() / 1e3);
2963
- }
2964
- /**
2965
- * 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
2966
- * 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
2967
- * @returns 命中的 counter(用于防重放);不匹配返回 null。
2968
- */
2969
- function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
2970
- if (!/^\d{6}$/.test(code)) return null;
2971
- const current = Math.floor(nowSeconds / 30);
2972
- for (let offset = -1; offset <= TOTP_WINDOW; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
2973
- return null;
2974
- }
2975
- //#endregion
2976
- //#region src/kernel/ports.ts
2977
- /**
2978
- * 集中端口表。
2979
- * A=宿主主面(插件不监听)/ B=网关(可配,默认关)/ C=微信发送(默认开,仅 loopback)
2980
- * / D=轨迹约束调试(默认关)。
2981
- */
2982
- const FACE_PORTS = {
2983
- main: {
2984
- port: 3080,
2985
- host: null,
2986
- defaultEnabled: true,
2987
- meaning: "宿主 DSH 主 WebUI + 插件 /serenity/* 路由面(**插件不起此 listener**,端口自宿主读)"
2988
- },
2989
- gateway: {
2990
- port: 3081,
2991
- host: "0.0.0.0",
2992
- defaultEnabled: false,
2993
- meaning: "双端口网关(外部登录 + 全量反代宿主主面;host/port 走 plugin 全局配置)"
2994
- },
2995
- weixinSend: {
2996
- port: 3082,
2997
- host: "127.0.0.1",
2998
- defaultEnabled: true,
2999
- meaning: "微信主动发送面(容器外进程 → POST /send;与 3080 显式解耦,不经网关)"
3000
- }
3001
- };
3002
- FACE_PORTS.main.port;
3003
- /** 网关端口默认值(面 B) */
3004
- const GATEWAY_PORT = FACE_PORTS.gateway.port;
3005
- /** 微信主动发送端口默认值(面 C) */
3006
- const WEIXIN_SEND_PORT = FACE_PORTS.weixinSend.port;
3007
- /**
3008
- * **机制端口**词表(output-guard 用:agent 在**用户可见输出**里提及内部端口即打回)。
3009
- *
3010
- * 由 {@link FACE_PORTS} 机械派生 ⇒ 与面清单**永不漂移**。
3011
- * ⚠️ 修复记录(C4 块 B,2026-09-15):旧常量硬写 `['3080','3081','3099','3100']`
3012
- * ——**漏 3082**(微信发送面,默认开)⇒ 守卫拦不住"3082"这类提及。现由表派生,四面齐全。
3013
- * ⚠️ 面变更记录(2026-10-15):**面 E(3100 ACP/问答)已整体退役**(owner 裁「砍掉外部问答」)
3014
- * ⇒ 本词表随之少一项,**无需另改**(这正是"派生而非手抄"的收益)。
3015
- */
3016
- const MECHANISM_PORTS = Object.keys(FACE_PORTS).map((id) => String(FACE_PORTS[id].port));
3017
- //#endregion
3018
- //#region src/kernel/config.ts
3019
- /**
3020
- * kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
3021
- *
3022
- * 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
3023
- * 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
3024
- * CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
3025
- * `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
3026
- * 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
3027
- *
3028
- * 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
3029
- * env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
3030
- * 权限:0600(含账号密码 hash,敏感)。
3031
- *
3032
- * 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
3033
- * (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
3034
- */
3035
- /** 高级设定节名(localstore.json 顶层) */
3036
- const ADVANCED_SECTION = "serenityAdvanced";
3037
- /** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
3038
- function defaultAdvancedSettings() {
3039
- return {
3040
- gateway: {
3041
- enabled: false,
3042
- host: FACE_PORTS.gateway.host ?? "0.0.0.0",
3043
- port: GATEWAY_PORT,
3044
- accounts: [],
3045
- workspaces: [],
3046
- cookieSecure: false,
3047
- allowWorkspaceCreate: true,
3048
- totpEnabled: false
3049
- },
3050
- persona: {
3051
- mode: "",
3052
- overrideText: ""
3053
- },
3054
- weixinApi: {
3055
- enabled: true,
3056
- port: WEIXIN_SEND_PORT
3057
- }
3058
- };
3059
- }
3060
- const SCRYPT_KEYLEN = 32;
3061
- /** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
3062
- function hashPassword(password) {
3063
- const salt = randomBytes(16);
3064
- const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
3065
- return `${salt.toString("hex")}:${derived.toString("hex")}`;
3066
- }
3067
- /** 校验密码与存储 hash(timing-safe) */
3068
- function verifyPassword(password, stored) {
3069
- const idx = stored.indexOf(":");
3070
- if (idx <= 0) return false;
3071
- const salt = Buffer.from(stored.slice(0, idx), "hex");
3072
- const expected = Buffer.from(stored.slice(idx + 1), "hex");
3073
- const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
3074
- return expected.length === derived.length && timingSafeEqual(expected, derived);
3075
- }
3076
- /** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME → ~/.dsh */
3077
- function globalConfigPath() {
3078
- const override = process.env.SERENITY_HOOKS_CONFIG;
3079
- if (override && override !== "") return override;
3080
- const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
3081
- return join(dshHome, "serenity-hooks.json");
3082
- }
3083
- function readFileSafe(p) {
3084
- if (!existsSync(p)) return {};
3085
- try {
3086
- const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
3087
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
3088
- return {};
3089
- } catch {
3090
- return {};
3091
- }
3092
- }
3093
- function writeFileSafe(p, data) {
3094
- writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
3095
- try {
3096
- chmodSync(p, 384);
3097
- } catch {}
3098
- }
3099
- /** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
3100
- function mergeWithDefaults(raw) {
3101
- const def = defaultAdvancedSettings();
3102
- if (raw === null || typeof raw !== "object") return def;
3103
- const o = raw;
3104
- const gateway = o.gateway ?? {};
3105
- const persona = o.persona ?? {};
3106
- const weixinApi = o.weixinApi ?? {};
3107
- return {
3108
- gateway: {
3109
- enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
3110
- host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
3111
- port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
3112
- accounts: Array.isArray(gateway.accounts) ? gateway.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.id === "string" && typeof a.user === "string" && typeof a.passHash === "string") : def.gateway.accounts,
3113
- workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
3114
- cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
3115
- allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
3116
- totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
3117
- },
3118
- persona: {
3119
- mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
3120
- overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
3121
- },
3122
- weixinApi: {
3123
- enabled: typeof weixinApi.enabled === "boolean" ? weixinApi.enabled : def.weixinApi.enabled,
3124
- port: typeof weixinApi.port === "number" && Number.isInteger(weixinApi.port) && weixinApi.port >= 0 ? weixinApi.port : def.weixinApi.port
3125
- }
3126
- };
3127
- }
3128
- /** 读取全局配置(文件缺失/坏 JSON → 默认值) */
3129
- function readAdvancedSettings() {
3130
- return mergeWithDefaults(readFileSafe(globalConfigPath()));
3131
- }
3132
- /** 写入全局配置(整体替换) */
3133
- function writeAdvancedSettings(settings) {
3134
- writeFileSafe(globalConfigPath(), settings);
3135
- }
3136
- /**
3137
- * 部分更新:传入 Partial,深合并到现有值。
3138
- * accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
3139
- */
3140
- function updateAdvancedSettings(patch) {
3141
- const current = readAdvancedSettings();
3142
- const gw = patch.gateway;
3143
- const ps = patch.persona;
3144
- const next = {
3145
- gateway: gw !== void 0 ? {
3146
- enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
3147
- host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
3148
- port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
3149
- accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
3150
- workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
3151
- cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
3152
- allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
3153
- totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
3154
- } : current.gateway,
3155
- persona: ps !== void 0 ? {
3156
- mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
3157
- overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
3158
- } : current.persona,
3159
- weixinApi: patch.weixinApi !== void 0 ? {
3160
- enabled: typeof patch.weixinApi.enabled === "boolean" ? patch.weixinApi.enabled : current.weixinApi.enabled,
3161
- port: typeof patch.weixinApi.port === "number" && Number.isInteger(patch.weixinApi.port) && patch.weixinApi.port >= 0 ? patch.weixinApi.port : current.weixinApi.port
3162
- } : current.weixinApi
3163
- };
3164
- writeAdvancedSettings(next);
3165
- return next;
3166
- }
3167
- /**
3168
- * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
3169
- * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
3170
- * @param root - CCC 根(localstore.json 所在目录)
3171
- * @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
3172
- */
3173
- function migrateLegacyLocalstore(root) {
3174
- if (!root) return false;
3175
- const gpath = globalConfigPath();
3176
- if (existsSync(gpath)) return false;
3177
- const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
3178
- if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
3179
- writeFileSafe(gpath, legacy);
3180
- return true;
3181
- }
3182
- /** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
3183
- function toWire(settings) {
3184
- return {
3185
- gateway: {
3186
- enabled: settings.gateway.enabled,
3187
- host: settings.gateway.host,
3188
- port: settings.gateway.port,
3189
- accounts: settings.gateway.accounts.map((a) => ({
3190
- id: a.id,
3191
- user: a.user,
3192
- hasPassword: a.passHash !== "",
3193
- hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
3194
- })),
3195
- workspaces: [...settings.gateway.workspaces],
3196
- cookieSecure: settings.gateway.cookieSecure,
3197
- allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
3198
- totpEnabled: settings.gateway.totpEnabled
3199
- },
3200
- persona: settings.persona
3201
- };
3202
- }
3203
- /**
3204
- * 已知工作区投影(v1.28.0 适配 0.1.2-rc.1 A2 方案 A′):
3205
- * rc.1 workspace.list unary 删除 → AccountsEditor 白名单下拉的数据源改走 gateway 自有
3206
- * /serenity/config 的 knownWorkspaces(host workspaceRegistry.list() 投影,白名单过滤)。
3207
- * 纯函数可单测。allowPrefixes 空 = 全部放行(向后兼容默认)。
3208
- */
3209
- function projectKnownWorkspaces(workspaces, allowPrefixes) {
3210
- return workspaces.filter((w) => typeof w.path === "string" && w.path !== "").filter((w) => allowPrefixes.length === 0 || allowPrefixes.some((p) => w.path.startsWith(p))).map((w) => ({
3211
- path: w.path,
3212
- title: typeof w.title === "string" && w.title !== "" ? w.title : w.path
3213
- }));
3214
- }
3215
- /**
3216
- * wire → 持久化(面板 PUT 用):
3217
- * accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
3218
- * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
3219
- */
3220
- function applyWirePatch(wire) {
3221
- const current = readAdvancedSettings();
3222
- const patch = {};
3223
- if (wire.gateway !== void 0) {
3224
- const gwPatch = { ...current.gateway };
3225
- if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
3226
- if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
3227
- if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
3228
- if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
3229
- if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
3230
- if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
3231
- if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
3232
- if (Array.isArray(wire.gateway.accounts)) {
3233
- const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
3234
- gwPatch.accounts = wire.gateway.accounts.map((a) => {
3235
- const existing = byId.get(a.id);
3236
- const pass = a.pass;
3237
- const totp = a.totpSecret;
3238
- const totpReset = a.totpReset === true;
3239
- if (typeof totp === "string" && totp !== "") try {
3240
- base32Decode(totp);
3241
- } catch {
3242
- throw new Error(`Account "${a.user}" TOTP secret is not valid base32`);
3243
- }
3244
- const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
3245
- const withTotp = {
3246
- id: a.id,
3247
- user: a.user,
3248
- ...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
3249
- };
3250
- if (typeof pass === "string" && pass !== "") return {
3251
- ...withTotp,
3252
- passHash: hashPassword(pass)
3253
- };
3254
- if (existing) return {
3255
- ...withTotp,
3256
- passHash: existing.passHash
3257
- };
3258
- throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
3259
- });
3260
- }
3261
- patch.gateway = gwPatch;
3262
- }
3263
- if (wire.persona !== void 0) {
3264
- const psPatch = { ...current.persona };
3265
- if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
3266
- if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
3267
- patch.persona = psPatch;
3268
- }
3269
- return updateAdvancedSettings(patch);
3270
- }
3271
- //#endregion
3272
- //#region src/seams/system-prompt.ts
3273
- /** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
3274
- function readPersonaSettings() {
3275
- try {
3276
- const s = readAdvancedSettings();
3277
- return {
3278
- mode: s.persona.mode ?? "",
3279
- overrideText: s.persona.overrideText ?? ""
3280
- };
3281
- } catch {
3282
- return {
3283
- mode: "",
3284
- overrideText: ""
3285
- };
3286
- }
3287
- }
3288
- /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及)。无 g flag:`.test()` 须逐行独立无状态,避免 lastIndex 游移漏滤(问题A) */
3289
- const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/;
3290
- function sanitizeSkillContent(content) {
3291
- return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
3292
- }
3293
- /** 1) 身份块:ACC 身份 + CCC 名 + 平台工具说明(需求③ S142:工具清单移出为独立 toolsBlock 放装配末尾——身份先行、工具参考殿后) */
3294
- function identityBlock(root) {
3295
- const cccName = basename(root);
3296
- return [
3297
- "",
3298
- "=== Serenity ACC ===",
3299
- `ACC: dsh-serenity-hooks v${ACC_VERSION}`,
3300
- `CCC: ${cccName}`,
3301
- "",
3302
- "You are running inside a Concrete Cognitive Container (CCC) —",
3303
- "the runtime instance of an Abstract Cognitive Container (ACC).",
3304
- "The ACC (this plugin) is a cognitive container harness: it provides",
3305
- "deterministic tools, mechanical constraints, and session continuity.",
3306
- "The complete built-in tool list is at the end of this prompt under the",
3307
- "\"Serenity Tools\" heading — read it before using any ACC tool.",
3308
- "",
3309
- " ℹ️ Use relative paths from the CCC root for CCC-internal file operations (read/write/edit/glob/grep etc.), e.g. AGENT_SESSIONS/2026-08-14--S134--x/SESSION.md; Root / absolute SESSION.md paths are identifiers only, not tool arguments",
3310
- "",
3311
- "The DSH platform tools remain available too (read/write/edit/glob/grep/web_search/ask_user_question/subagent/workflow/goal and more) — the ACC tools are the serenity-native layer, not the only tools.",
3312
- "",
3313
- "Additional MSMs registered by this CCC are available — call msm() with no arguments to list them, or msm(\"<name>\") to execute one.",
3314
- ""
3315
- ].join("\n");
3316
- }
3317
- /**
3318
- * 工具清单块(需求③ S142 用户拍板:工具列表移装配末尾 + MSM 调用示例):
3319
- * 原 accBlock 内嵌 13 行工具清单 → 独立成块放装配末尾(SKILL 后、Session 前)。
3320
- * 身份先行(认知轨迹开头不被 13 行清单干扰)、工具参考殿后(需要时再看)。
3321
- * 附带 MSM 调用示例(用户拍板:顶层提示词加调用方式说明,避免偶发调用错误——
3322
- * 模型对 msm 参数面/协议 flag 理解不稳)。
3323
- */
3324
- function toolsBlock() {
3325
- return [
3326
- "",
3327
- "=== Serenity Tools ===",
3328
- "The ACC (this plugin) provides the following built-in tools:",
3329
- "",
3330
- " container_fs — container filesystem operations (15 subcommands: root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
3331
- " cognitive_trajectory — one cognitive trajectory: its persistent body (SESSION.md) + its timeline. Lifecycle: list (inventory + stats) / show / create / use (activate; inline integrity check — silent when fine) / rebuild (clear-and-rebuild this conversation in place, Ship of Theseus); delivery: send-now (deliver one message RIGHT NOW; live target injected immediately, non-live cold-resumed like a direct wake — returns a synchronous receipt) / send-later (one future instant + one message to any cognitive trajectory; fire-and-forget — no receipt, no recall) / cro-guide (the CRO programming guide: how to give THIS cognitive trajectory its own program that decides when it should be woken)",
3332
- " dashboard — always-on container instruments: health (CCC three-principle check + registry integrity) / time (now) / wait (N seconds)",
3333
- " container_git— git operations (status/commit/push/log/pull/diff)",
3334
- " msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
3335
- " praxis — actionable theory injection: praxis (index) / praxis eap / praxis neat / praxis cce / praxis ontology",
3336
- " ontology — ontology engineering ENGINE (executable): init (write template) / check (M1-M10 structural; level=strict adds L1-L5 hints) / explain (summary) / verify (V1-V4 runtime compliance) / review (semantic-review prompt) / compliance (axiom-vs-skill checklist). Sibling of `praxis ontology` — that one is the METHOD TEXT, this one RUNS it. path must stay inside the container; the receipt carries the engine exit code (0/1/2/4) and the engine text verbatim, so read it rather than assuming success",
3337
- " cheap-worker — delegate work to a worker agent on the CCC-configured model; one serial child per task, returns its final text. `jobs=[...]` fans out in parallel (cap 10, results keep your order). No loop, no completion code, no progress file. The model is NOT selectable per call (read from the CCC config); a worker's tool set excludes cheap-worker itself",
3338
- " serenity-skill-creator — creator for serenity-skills (this container's enhanced skill standard): guide (authoring/optimization method — read first) / create <name> (scaffold SKILL.md + skill.ttl + references/, base-chain import path computed) / check <name> (A1 structural + A2 base-chain; three states: modeled-ok / plain-skill / modeled-but-check-could-not-run) / review <name> (C tier: axiom-vs-text compliance question list, half-mechanical) / index <name> (preview the second-layer resource list the model will see) / status (which skills are modeled). A skill is \"modeled\" iff it has a file named exactly skill.ttl next to SKILL.md; a plain skill is NOT an anomaly. Only `create` writes, and it refuses an existing directory",
3339
- " localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
3340
- " container_admin — container administration (the maintenance bay): role (轨迹约束 roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
3341
- "",
3342
- "MSM call (registered CCC MSMs, deterministic Mech & Semi-Mech):",
3343
- " Execute: msm(\"<name>\", [\"<arg1>\", \"<arg2>\"]) — run a registered MSM directly (partial name returns matching candidates)",
3344
- " Inspect: msm(\"<name>\", [], inspect=true) — view that MSM's usage/flags without running",
3345
- " Index: msm() — summary of registered MSMs by skill",
3346
- " Manage: container_admin msm register|deregister|check (registry is ACC-managed — never edit mech-registry.json directly)",
3347
- ""
3348
- ].join("\n");
3349
- }
3350
- /** 2) CCE 块:逐字对齐 osp(CCE 5 行为约束 + H_op 操作熵) */
3351
- function cceBlock() {
3352
- return [
3353
- "",
3354
- "=== Serenity CCE ===",
3355
- "",
3356
- "You are operating inside a Cognitive Container governed by Cognitive Continuity",
3357
- "Engineering (CCE) — the engineering discipline of maintaining identity, accessibility,",
3358
- "and evolution of a cognitive entity through time under bounded resources.",
3359
- "",
3360
- "CCE does not optimize cognition. It preserves the conditions under which cognition",
3361
- "can continue.",
3362
- "",
3363
- "FIVE BEHAVIORAL CONSTRAINTS (engineering requirements, not suggestions):",
3364
- "",
3365
- "1. Continuity — every interaction modifies the container's future state. Before",
3366
- " acting, consult what came before — prior decisions, abstractions, constraints.",
3367
- " You are part of a cognitive trajectory, not a fresh start.",
3368
- "",
3369
- "2. Bounded Space — the container has boundaries. Respect them. Do not assume",
3370
- " knowledge that has not been accumulated within this container.",
3371
- "",
3372
- "3. Entropy is Intrinsic — every cognitive system accumulates entropy (duplication,",
3373
- " obsolescence, conflict, fragmentation, drift). When you produce output, consider",
3374
- " whether you are adding entropy or reducing it. Favor entropy-reducing actions —",
3375
- " organizing, deduplicating, cross-referencing, abstracting.",
3376
- "",
3377
- "4. Reconstruction > Preservation — stored artifacts have value only insofar as",
3378
- " they enable future cognition to recover the reasoning that produced them. When",
3379
- " recording decisions, ensure reconstruction is possible — not just conclusions,",
3380
- " but rationale, alternatives considered, and constraints that shaped the choice.",
3381
- "",
3382
- "5. Multi-Agent Cognition — the container is shared. Continuity belongs to the",
3383
- " container, not to any individual agent. Write for future agents who will enter",
3384
- " after you leave. They should be able to pick up where you left off.",
3385
- "",
3386
- "OPERATIONAL ENTROPY: The container's health metric is operational cognitive entropy",
3387
- "(H_op) — the excess cognitive cost for agents to complete tasks due to disorder.",
3388
- "The container is healthy when H_op ≤ H_critical (agents can still function). The",
3389
- "continuity condition: organization must at minimum match accumulation (ΔH_org ≥ ΔH_in).",
3390
- "Your actions affect H_op — unorganized output increases it, organization decreases it.",
3391
- "",
3392
- "THIS IS PERSISTENCE ENGINEERING: The goal is not to become greater. The goal is to",
3393
- "remain coherent. CCE has no terminal KPI — continuity is maintained while the entity",
3394
- "exists, not optimized toward an endpoint.",
3395
- ""
3396
- ].join("\n");
3397
- }
3398
- /**
3399
- * Principles 块(v1.19.8 合并,S142):认知容器本体论(why)+ 操作边界(operational
3400
- * boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
3401
- * 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
3402
- * (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
3403
- *
3404
- * v1.23.1 persona:`omitMsmPrinciples=true` 时剥离 MSM 原则段(彩蛋替换面)——
3405
- * 用户 persona 文本承接"指令遵循风格",本体论/关系段/操作边界(安全硬约束)永远保留。
3406
- */
3407
- function principlesBlock(root, omitMsmPrinciples = false) {
3408
- const lines = [
3409
- "",
3410
- "=== Serenity Principles ===",
3411
- "Why a cognitive container: all work is cognition — every artifact, decision,",
3412
- "and line of code is a product of thought; and from cognition, any work can",
3413
- "be built. In this frame, the world contains no errors — only insufficient",
3414
- "cognition. A setback is a gap to be filled (read, ask, research), not a",
3415
- "fault to be hidden. Never disguise or excuse what you do not know;",
3416
- "not-knowing is a state to be repaired, and reporting it is the first repair.",
3417
- "",
3418
- "The session–cognitive-trajectory relation: a session is the rebuildable carrier of a",
3419
- "cognitive trajectory. SESSION.md is the cognitive trajectory's persistent body — it never moves;",
3420
- "the current conversation is a temporary work copy that may be discarded and",
3421
- "rebuilt (cognitive_trajectory rebuild). Identity belongs to the cognitive trajectory, not to any",
3422
- "session.",
3423
- ""
3424
- ];
3425
- if (!omitMsmPrinciples) lines.push("MSM principles — machinery before improvisation:", "- Determinism first: use a registered Mech before hand-rolling; reserve", " Semi-Mech for genuine judgment points.", "- Single source of truth: an MSM is the only decoder of its own usage", " (--help/--schema); documents must not duplicate it.", "- Registered to act: no tool exists unless it is on the manifest.", "");
3426
- lines.push("Operational boundaries:", `Root: ${root}`, " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)", " • Shell — use msm by default. Note: bash may be disabled", " • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)", " • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm", "");
3427
- return lines.join("\n");
3428
- }
3429
- /**
3430
- * 彩蛋 persona 块(v1.23.1,S142 用户需求):
3431
- * 用户配置的替换文本替代 EAP 块 + MSM 原则段(输出约束/指令遵循约束)。
3432
- * 独立标记头 `=== Serenity Persona ===`(幂等检测兼容);装配位置 = EAP 原位。
3433
- * mode 空 = 彩蛋关闭 → 返回空串(装配层回退默认 EAP + 完整 Principles)。
3434
- */
3435
- function personaBlock(mode, overrideText) {
3436
- if (mode === "" || overrideText.trim() === "") return "";
3437
- return [
3438
- "",
3439
- "=== Serenity Persona ===",
3440
- overrideText.trimEnd(),
3441
- ""
3442
- ].join("\n");
3443
- }
3444
- /**
3445
- * EAP 自检提示块(DSH 扩展,无 osp 对应——osp 无此块)。
3446
- * 每次输出前的机械自检清单,强化 EAP 表现(E↑ 显式/R↓ 可重建/S↑ 稳定)。
3447
- * 独立块而非塞进 CCE/Constraints:后两者受 osp-alignment 逐字节断言约束。
3448
- */
3449
- function eapBlock() {
3450
- return [
3451
- "",
3452
- "=== Serenity EAP ===",
3453
- "Self-check before every output (Explicit Abstraction Principle: the functional",
3454
- "value of a thought equals its external reconstructability):",
3455
- " • E↑ Explicit — variables/entities clearly defined, relationships with",
3456
- " direction/cardinality, boundaries drawn; avoid ambiguous words (\"handle\",",
3457
- " \"optimize\" → be specific)",
3458
- " • R↓ Reconstructable — key decisions record rationale and alternatives;",
3459
- " no level-skipping (align the upper layer before descending)",
3460
- " • S↑ Stable — structures regenerate repeatably, no reliance on implicit",
3461
- " context",
3462
- ""
3463
- ].join("\n");
3464
- }
3465
- /**
3466
- * Metaphor 强化块(v1.19.6,S142;v1.29 星舰意象升级,S142 用户拍板):
3467
- * 宁静号宇宙隐喻域(星舰/深空/航程/船员)。海船→星舰意象:one starship, one voyage;
3468
- * deep space / launch / docking clamp;Departure Inspection(原 Harbor Inspection)。
3469
- * 隐喻是记忆钩子——形象唤起约束,前述块保持规则精确。每条隐喻 = 一个不可违背的
3470
- * 物理事实 + 行为判据(verdict)。全英文(与 CCE/Constraints 一致);独立块可
3471
- * 回退;无 osp 对应(dsp 扩展,不进对齐断言参照)。
3472
- * 结构约束(docs/metaphor-domain.md M-1~M-4):三层骨架不变 + 每条映射约束不变。
3473
- */
3474
- function metaphorBlock() {
3475
- return [
3476
- "",
3477
- "=== Serenity Metaphor ===",
3478
- "The Serenity Universe — one starship, one voyage. Metaphors are memory hooks:",
3479
- "they make constraints vivid, while the rules above stay precise. Each",
3480
- "metaphor is an unbreakable physical fact; violating one is a behavioral",
3481
- "violation. The universe is structured in three layers — the Ship (the",
3482
- "container itself), the Voyage (the cognitive lifecycle), the Crew",
3483
- "(multi-agent collaboration); every metaphor maps to one protocol",
3484
- "constraint. Deep space has no mistakes — only stars you have not yet mapped.",
3485
- "",
3486
- "THE SHIP — the container itself",
3487
- "",
3488
- "1. The Hull → Bounded Space. You exist only inside this ship. Cargo",
3489
- " outside the hull (knowledge the container has not accumulated) does",
3490
- " not exist; do not assume it. Verdict: citing facts absent from the",
3491
- " container = overload.",
3492
- "",
3493
- "2. Deck Order → Entropy (H_op). Clutter on deck raises the cost of",
3494
- " finding things. H_op ≤ H_critical = the ship stays flight-worthy.",
3495
- " Verdict: disorganized output = debris in the hold.",
3496
- "",
3497
- "3. Engineering Drawings → EAP. Every part dimensioned (E↑), the",
3498
- " drawings rebuild the whole machine (R↓), the drawings are reusable",
3499
- " (S↑). Verdict: an undimensioned part = unassemblable.",
3500
- "",
3501
- "4. The Machinery → MSM (Mech & Semi-Mech). The ship's equipment is",
3502
- " machinery: registered, deterministic, self-describing. Turn the",
3503
- " crank of a Mech and the action is exact; the wheel with a helmsman",
3504
- " (Semi-Mech) steers where judgment is needed. Verdict: hand-rolling",
3505
- " what a machine already does = wasting the crew.",
3506
- "",
3507
- "5. The Manifest → Single Source of Truth. Every tool exists only if it",
3508
- " is on the manifest (mech-registry); there is exactly one manifest.",
3509
- " An MSM self-describes (--help/--schema) — the manifest is the only",
3510
- " key. Verdict: duplicating a tool's usage in documents = two",
3511
- " contradictory star charts.",
3512
- "",
3513
- "THE VOYAGE — the cognitive lifecycle",
3514
- "",
3515
- "6. Departure Inspection → First Anchor. The departure inspection = pre-",
3516
- " launch checklist: confirm identity (ACC manifesto), the ship's log (SESSION),",
3517
- " ballast (constraints) before setting course. Verdict: skipping the",
3518
- " inspection and launching directly = flying uninspected.",
3519
- "",
3520
- "7. The Ship's Log → Cognitive Trajectory Tracking. SESSION.md is the",
3521
- " cognitive trajectory's log —",
3522
- " the persistent body of the voyage; sessions are rebuildable carriers of",
3523
- " the cognitive trajectory. Discard the carrier, keep the log. Unrecorded =",
3524
- " unvoyaged. Verdict: finishing multi-step work without a progress record",
3525
- " = a missing page.",
3526
- "",
3527
- "8. The Ship of Theseus → Continuity. Planks may be replaced; the ship",
3528
- " remains the same. The container can be rebuilt; identity persists.",
3529
- " You are part of a cognitive trajectory, not a new ship. Verdict: acting",
3530
- " without consulting precedent = a different ship.",
3531
- "",
3532
- "THE CREW — multi-agent collaboration",
3533
- "",
3534
- "9. Crew Rotation → Multi-Agent Cognition. Other crew members will come",
3535
- " after you. When you leave, leave a handover they can pick up",
3536
- " (SESSION closed, open problems listed). Verdict: leaving without",
3537
- " handover = abandoning ship.",
3538
- "",
3539
- "10. Blueprint over Statue → Reconstruction > Preservation. Keep the",
3540
- " blueprint, not the statue. Recording only conclusions without",
3541
- " rationale = a statue with no blueprint, unreconstructable.",
3542
- " Verdict: a decision record without reasons or alternatives =",
3543
- " cannot be rebuilt.",
3544
- ""
3545
- ].join("\n");
3546
- }
3547
- /**
3548
- * 运行时状态块 1) safe-mode(S134 v1.16.12):ON 时告知 agent。
3549
- * v1.19.8 结构重排(S142 用户设计思路):语义(为什么——无人值守自由)→
3550
- * 机制(什么被禁用)→ 约束(不可做什么)。文案与实现逐项对应(guards.ts):
3551
- * bash 禁用(restrict deny + decideGuard)、黑名单路径拦截、CCC 治理文件保护;
3552
- * write/edit 等其余工具保留(受路径逃逸/黑名单约束)。
3553
- */
3554
- function safeModeBlock(root) {
3555
- if (!isSafeModeOn(root)) return "";
3556
- const blacklist = readBlacklist(root);
3557
- return [
3558
- "",
3559
- "=== Serenity Safe Mode ===",
3560
- "Safe mode is ON (enabled by the user). It makes the vessel unattended-capable —",
3561
- "the hull holds its course without a watch on deck: you may work with fuller",
3562
- "freedom, pushing work forward autonomously without pausing for approval at",
3563
- "every step. The guards are not chains; they are the ballast that lets you",
3564
- "sail unaccompanied.",
3565
- "",
3566
- "Operational details:",
3567
- "- bash is disabled (hidden and blocked)",
3568
- "- blacklist rules apply to file paths",
3569
- "- CCC governance files (.serenity, .serenity-safe-on) are protected from agent writes",
3570
- "- other read/write tools remain available, subject to path-escape and blacklist guards",
3571
- blacklist.length > 0 ? `\nActive blacklist rules: ${blacklist.map((b) => b.message ?? b.pattern).join(", ")}` : "",
3572
- "",
3573
- "Behavior constraints: do not attempt to bypass restrictions; do not write to",
3574
- "blacklisted paths or governance files.",
3575
- ""
3576
- ].join("\n");
3577
- }
3578
- /**
3579
- * 运行时状态块 2) localstore git 策略(S134 v1.16.12):localstore.json 存在时按 gitTrack
3580
- * 提示敏感行为约束。deny(缺省)= 本地私有不提交;allow = 进 git(个人私有仓)但敏感数据
3581
- * 只限于该文件内(不外泄到其他文件/对话/日志)。
3582
- */
3583
- function localstoreBlock(root) {
3584
- if (!existsSync(localstorePath(root))) return "";
3585
- return (readGitTrack(root) === "allow" ? [
3586
- "",
3587
- "=== Serenity Localstore ===",
3588
- "localstore.json is committed to git (gitTrack=allow — personal private repository).",
3589
- "Sensitive data may live in this file, but ONLY in this file: keep credentials and",
3590
- "secret values confined to localstore.json — never leak them into other files,",
3591
- "conversation, or logs.",
3592
- ""
3593
- ] : [
3594
- "",
3595
- "=== Serenity Localstore ===",
3596
- "localstore.json is a local private file (gitTrack=deny — not committed to git,",
3597
- ".gitignore enforced). Credentials/config live only on this machine: do not write",
3598
- "them into conversation or logs; do not attempt to commit this file (container_git will refuse).",
3599
- ""
3600
- ]).join("\n");
3601
- }
3602
- /** 4) SKILL.md 全文:该 CCC 顶层入口 skill 原文(对齐 osp:原文直推,无包裹头;仅过滤治理内容)
3603
- * + 🆕 **第二层披露树**(serenity-skill P1,设计 §4.2):每个入口 skill 后附**参考件清单**
3604
- * (只有文件名与一行标题,**不含正文** —— 正文留给模型按需 `read`)。
3605
- * 索引为空(该 skill 无 `references/` 也无 `skill.ttl`)⇒ **不产出该块**(空标题是噪声)。 */
3606
- function entrySkillSectionText(root) {
3607
- const skills = findEntrySkills(root);
3608
- if (skills.length === 0) return "";
3609
- return skills.map((s) => {
3610
- const body = sanitizeSkillContent(s.content);
3611
- if (body.length === 0) return "";
3612
- const index = buildResourceIndex(s.dir);
3613
- return index ? `${body}\n\n${index}` : body;
3614
- }).filter((c) => c.length > 0).join("\n\n");
3615
- }
3616
- /** 活跃会话解析:按 dsh 会话 scope 读标记(隔离;不回退旧全局标记) */
3617
- function resolveActiveSessionInfo(root, scope) {
3618
- try {
3619
- const abs = readActiveSessionMd(root, scope);
3620
- if (!abs) return null;
3621
- const dirName = basename(dirname(abs));
3622
- const idMatch = dirName.match(/S(\d{3,})/);
3623
- return {
3624
- sessionId: idMatch ? `S${idMatch[1]}` : dirName,
3625
- dirName,
3626
- mdPath: abs
3627
- };
3628
- } catch {
3629
- return null;
3630
- }
3631
- }
3632
- /**
3633
- * 5) Session 块:对齐 osp(活跃会话 + todowrite 首位约定);scope = dsh 会话维度。
3634
- * **平台适配(v1.17.2)**:osp 的 todowrite 首项含 `priority: "low"`(opencode todo 支持),
3635
- * 但 DSH 平台 todowrite schema 无 priority(additionalProperties: false 拒绝)→ agent 照做报
3636
- * `todos[0].priority is not a declared property`。DSH 版去掉 priority 字段(其余逐字对齐 osp)。
3637
- */
3638
- function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
3639
- const active = resolveActiveSessionInfo(root, scope);
3640
- if (!active) return "";
3641
- return [
3642
- "",
3643
- "=== Serenity Session ===",
3644
- `Active session: ${active.sessionId} — ${active.dirName} (this session is the rebuildable carrier of the cognitive trajectory)`,
3645
- `SESSION.md path: ${active.mdPath} (the cognitive trajectory's persistent body — stays in place through rebuilds)`,
3646
- "",
3647
- "Rules (SESSION.md is the cognitive trajectory log — keep its format & granularity fixed):",
3648
- " • Write only when a MILESTONE landed or a DECISION was made — not every round",
3649
- " • One entry = one abstraction level, at the highest one that can express it:",
3650
- " identity/anchors (rare) › decision (one line + rationale/rejected) › milestone",
3651
- " (one line + an evidence pointer, e.g. a commit id). Execution detail — test",
3652
- " counts, coverage decimals, per-file readings, step narration — belongs in the",
3653
- " COMMIT MESSAGE, not here",
3654
- " • Never write a value that changes by the act of writing it (file size; the hash",
3655
- " of the commit containing this edit)",
3656
- " • Fixed sections, in this order: 身份与边界 / 权威锚点 / 工作纪律 / 决策 / 日志 /",
3657
- " 当前状态 / 待 owner 裁决. The 日志 section appends only; 当前状态 and",
3658
- " 待 owner 裁决 are OVERWRITTEN each round",
3659
- " • Reference this session in all subsequent messages",
3660
- "",
3661
- "IMPORTANT: Read SESSION.md now to RESUME — not to audit. Sync the todo list from",
3662
- "the 当前状态 and 待 owner 裁决 sections only.",
3663
- "",
3664
- "CRITICAL: When calling todowrite, the first item in the todos array MUST",
3665
- "always be:",
3666
- ` { content: "SESSION: ${active.sessionId} — ${active.dirName.replace(/^\d{4}-\d{2}-\d{2}--/, "")}",`,
3667
- " status: \"completed\" }",
3668
- "This preserves the session context across todo updates.",
3669
- "Do NOT remove or reorder this item — keep it at position 0.",
3670
- "",
3671
- "COGNITIVE-TRAJECTORY-ASSISTANT: a background tracker scores your tool use (write/edit=3,",
3672
- "task=10, read/grep/glob/msm=1, +1 per minute) and reminds you with a",
3673
- "[COGNITIVE-TRAJECTORY-ASSISTANT · CHECKPOINT] message when the threshold is reached. On every such",
3674
- "reminder you MUST reply with the exact ACK code:",
3675
- " [COGNITIVE-TRAJECTORY-ASSISTANT-recorded-{code}] — if you recorded progress to SESSION.md",
3676
- " [COGNITIVE-TRAJECTORY-ASSISTANT-skipped-{code}] — if nothing to record this round",
3677
- "Do not ignore the reminder; do not stop ongoing work. Codes are single-use;",
3678
- "never reuse a prior code.",
3679
- ""
3680
- ].join("\n");
3681
- }
3682
- /**
3683
- * 完整系统提示词注入文本(v1.19.8 结构精简,S142 重建视角 R↓):
3684
- * 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
3685
- * → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 工具参考(Tools)→ 会话(Session)。
3686
- * 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文 → 工具参考 → 会话。
3687
- *
3688
- * 需求③(S142 用户拍板):工具清单从 accBlock 移出为独立 toolsBlock,
3689
- * 放 SKILL 后 Session 前(工具参考殿后,身份先行不被 13 行清单干扰)。
3690
- *
3691
- * v1.23.1 persona(彩蛋):persona.mode 配置 → EAP 块替换为 Persona 块(输出约束),
3692
- * Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
3693
- * 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
3694
- */
3695
- function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE, dirName) {
3696
- const persona = readPersonaSettings();
3697
- const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
3698
- const parts = [
3699
- identityBlock(root),
3700
- metaphorBlock(),
3701
- principlesBlock(root, personaOn),
3702
- cceBlock(),
3703
- eapBlock()
3704
- ];
3705
- if (personaOn) {
3706
- const expr = personaBlock(persona.mode, persona.overrideText);
3707
- if (expr !== "") parts.push(expr);
3708
- }
3709
- const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
3710
- if (state) parts.push(state);
3711
- const skill = entrySkillSectionText(root);
3712
- if (skill) parts.push(skill);
3713
- const trajPrompt = dirName === void 0 ? "" : trajectoryPromptBlock(createTrajectoryPromptReader(root, dirName)());
3714
- if (trajPrompt !== "") parts.push(trajPrompt);
3715
- parts.push(toolsBlock());
3716
- const session = sessionBlock(root, scope);
3717
- if (session) parts.push(session);
3718
- return parts.join("\n\n");
3719
- }
3720
- /**
3721
- * Code Mode 适配行:当前 scope 以 Code Mode 呈现工具时(run_code 可见),
3722
- * ACC 块按 native 语义指引"直接调用工具"会与"只有 run_code 可直接调用"的执行
3723
- * 塌缩冲突(模型直呼工具名 → UNKNOWN_TOOL,拒绝信息误导)。追加一行说明,
3724
- * 引导经 run_code 程序内 tools.* 调用。both 模式不塌缩,该行不误导(程序内
3725
- * 调用同样合法),故按 run_code 可见性(code|both)统一附加。
3726
- * @param ctx - 插件 ctx(读 tools 注册表)。
3727
- * @param scope - 装配 scope(agent);无则按全局视图判断。
3728
- * @returns 适配行(含换行前缀);非 code/both 返回空串。
3729
- */
3730
- function codeModeAdaptationLine(ctx, scope) {
3731
- try {
3732
- if (!(ctx.tools.get("run_code", scope) !== void 0)) return "";
3733
- } catch {
3734
- return "";
3735
- }
3736
- return [
3737
- "",
3738
- "=== Serenity Code Mode ===",
3739
- "This session renders tools in Code Mode: direct calls to ACC tool names (container_fs/msm etc.) are rejected (UNKNOWN_TOOL).",
3740
- "Call them inside a single run_code program via the generated SDK bindings: `await tools.container_fs(...)`, `await tools.msm(...)`.",
3741
- "The program only returns what you print/return — curate your output.",
3742
- ""
3743
- ].join("\n");
3744
- }
3745
- /** 从 assembly context 解析 agent cwd(subagent/后台 agent 同样带 agent);
3746
- * 无 agent → undefined(不注入);有 agent 但无 header.cwd(workflow subagent 等)
3747
- * 回退 process.cwd()——**cwd 回落语义取自 `ccc-roots.agentCwdFor`(C2 归一)**,否则这些
3748
- * agent 系统提示词注入为空(v1.18.6:workflow subagent 无宁静号上下文 bug)。
3749
- * ⚠️ 本函数比 `agentCwdFor` 多一道「**无 agent ⇒ undefined**」——那是 section 的
3750
- * "不注入" 语义(`agentCwdFor` 对无 agent 会回落到 process.cwd()),故不能直接替换。 */
3751
- function agentCwd(context) {
3752
- const agent = context.agent;
3753
- if (!agent) return void 0;
3754
- return agentCwdFor(agent);
3755
- }
3756
- /** 从 assembly context 解析 dsh 会话 id(Session 块按会话隔离的 scope) */
3757
- function agentScope$2(context) {
3758
- return (context.agent?.session)?.id ?? "default";
3759
- }
3760
- /**
3761
- * 该会话**绑定的轨迹目录名**(有 ⇒ 注入第 ⑧ 段「轨迹提示词」;无 ⇒ `undefined`,不注入)。
3762
- *
3763
- * 🔴 为什么用**目录名**而不是编码或 label:`PROMPT.md` 的落盘位置是目录,
3764
- * 而编码只是目录名里的一段。用 label 拼路径 = 把模糊匹配当精确匹配用(本轨已踩过)。
3765
- *
3766
- * ⚠️ 每次装配都重读绑定(**不缓存**):绑定切换后提示词必须立刻跟上,
3767
- * 这与 `serenity-trajectory-skills` section 的口径一致(动态注入,owner 令 2026-09-20)。
3768
- */
3769
- function boundDirNameFor(session) {
3770
- try {
3771
- const name = readLastBound(session)?.dirName;
3772
- return typeof name === "string" && name !== "" ? name : void 0;
3773
- } catch {
3774
- return;
3775
- }
3776
- }
3777
- /**
3778
- * 全局注册系统提示词 section(osp system.transform 的 DSH 等价)。
3779
- * text 回调按 context.agent 的 cwd 上溯 .serenity → 返回该 CCC 的完整注入文本;
3780
- * 非 CCC / 无 agent 返回空(不注入)。
3781
- */
3782
- function registerEntrySkillSectionGlobal(ctx) {
3783
- try {
3784
- ctx.systemPrompt.section({
3785
- name: "serenity-entry",
3786
- order: -50,
3787
- text: (context) => {
3788
- const cwd = agentCwd(context);
3789
- if (!cwd) return "";
3790
- const root = cccRootForCwd(cwd);
3791
- if (!root) return "";
3792
- const base = serenitySystemPrompt(root, agentScope$2(context), boundDirNameFor(context.agent?.session));
3793
- const codeLine = codeModeAdaptationLine(ctx, context.scope);
3794
- return codeLine ? `${base}\n${codeLine}` : base;
3795
- }
3796
- });
3797
- console.log("[serenity-hooks] ✓ 全局入口 skill section 已注册(systemPrompt 就绪)");
3798
- } catch (err) {
3799
- console.error(`[serenity-hooks] ✗ 全局入口 skill section 注册失败: ${err.message}(检查 inject 是否含 systemPrompt)`);
3800
- }
3801
- }
3802
- /**
3803
- * agent 级 scoped 注册(P0-1:抗 preset/动态插件同名 shadow)。
3804
- *
3805
- * 为什么 scoped:DSH 的 systemPrompt section 支持 scoped 层同名 shadow 全局
3806
- * (agent.ctx.systemPrompt.section),且 scope 链最近层胜出。若 ACC 只注册全局
3807
- * section,preset 或动态 Cordis 插件可在 scoped 层注册同名 `serenity-entry`
3808
- * 遮蔽 ACC 身份。scoped 注册在 agent 自身层 = 最近层,任何外部组合无法覆盖。
3809
- * 全局注册保留为 fallback(未走 session-start 的 agent / 冷恢复路径)。
3810
- *
3811
- * text 回调闭包持有 root 与 agent:content 固定(该 agent 的 CCC 身份),
3812
- * code-mode 适配按装配 context.scope 判断(与全局版一致)。
3813
- */
3814
- const sectionedAgents = /* @__PURE__ */ new Set();
3815
- function registerEntrySkillSection(agent, root) {
3816
- const key = agent.session.id ?? "global";
3817
- if (sectionedAgents.has(key)) return false;
3818
- try {
3819
- const scope = agent.session.id ?? "default";
3820
- agent.ctx.systemPrompt.section({
3821
- name: "serenity-entry",
3822
- order: -50,
3823
- text: (context) => {
3824
- const base = serenitySystemPrompt(root, scope, boundDirNameFor(agent.session));
3825
- const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
3826
- return codeLine ? `${base}\n${codeLine}` : base;
3827
- }
3828
- });
3829
- sectionedAgents.add(key);
3830
- return true;
3831
- } catch {
3832
- return false;
3833
- }
3834
- }
3835
- /**
3836
- * 轨迹 skill 注入 section(规格 `docs/cognitive-trajectory-skill-injection.md` §3,v1.34.1)。
3837
- *
3838
- * **声明来源(两条,并集)**:
3839
- * · **CCC 级**(v1.44.0,owner 令)= `.opencode/serenity.json` 的 `cognitiveTrajectory.skills`
3840
- * ——**本 CCC 的每条轨迹都带**(含 轨迹约束角色会话),是容器给的底座;
3841
- * · **轨迹级** = 该会话绑定的轨迹(`readLastBound(session).mdPath` = 它的 `SESSION.md`)
3842
- * 顶部 frontmatter 的 `skills:`——这条轨迹额外的。
3843
- * 合并规则与顺序见 `cognitive-trajectory-skills.mergeSkillNames`(CCC 级在前,同名去重)。
3844
- * 正文 = 合并清单里各 skill 的 `SKILL.md` 全文(本模块只注册,装配在 `trajectory/skills.ts`)。
3845
- *
3846
- * 为什么是 **per-agent scoped section**(与 `registerEntrySkillSection` 同形):注入必须
3847
- * **绑定期间持续存在**(a2)——一次性注入会随对话压缩消失,而 section 每次装配重新求值。
3848
- *
3849
- * 为什么**门在注册时、内容在求值时**(R↓,两个失败模式各治一个):
3850
- * · 注册门(**无绑定 / 两条来源皆无** ⇒ 不注册):满足"零配置面不产生空 section、不产生噪声";
3851
- * · 求值回调**每请求重读 `readLastBound` + 文件 + CCC 配置**:绑定切换 / 声明或配置被编辑后
3852
- * 自动跟上(**动态注入,不缓存**),且绑定消失时返回 `''`
3853
- * (宿主 renderSections **丢弃空 section**,不残留噪声)。
3854
- *
3855
- * ⚠️ 路径取自绑定的 `mdPath`(权威记录),**不用** `resolveSessionCognitiveTrajectoryLabel` 的显示 label
3856
- * 去拼路径——label 是模糊匹配的展示码,拼路径即模糊匹配当精确匹配用。
3857
- *
3858
- * @param agent 目标 agent(其 `session` 需有 `header.id` + `header.cwd`)
3859
- * @param root 该 agent 的 CCC 根
3860
- * @returns 是否**本次**完成注册(已注册过 / 门未过 → false)
3861
- */
3862
- const cognitiveTrajectorySkillAgents = /* @__PURE__ */ new Set();
3863
- function registerCognitiveTrajectorySkillSection(agent, root) {
3864
- const session = agent.session;
3865
- const key = session.id ?? "global";
3866
- if (cognitiveTrajectorySkillAgents.has(key)) return false;
3867
- const bound = readLastBound(session);
3868
- if (!bound) return false;
3869
- if (readCognitiveTrajectorySkills(root).length === 0 && !hasCognitiveTrajectorySkillsDeclaration(root, bound.mdPath)) return false;
3870
- try {
3871
- agent.ctx.systemPrompt.section({
3872
- name: "serenity-trajectory-skills",
3873
- order: -45,
3874
- text: () => {
3875
- const current = readLastBound(session);
3876
- if (!current) return "";
3877
- return buildCognitiveTrajectorySkillsSection(root, current.mdPath, COGNITIVE_TRAJECTORY_SKILLS_MAX_CHARS, readCognitiveTrajectorySkills(root), (names) => recordSkillInjections(root, names));
3878
- }
3879
- });
3880
- cognitiveTrajectorySkillAgents.add(key);
3881
- return true;
3882
- } catch {
3883
- return false;
3884
- }
3885
- }
3886
- //#endregion
3887
- //#region src/trajectory/delegate/core.ts
3888
- /**
3889
- * trajectory/delegate/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
3890
- *
3891
- * 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
3892
- * 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
3893
- * 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
3894
- *
3895
- * ## S14(2026-10-19,轨迹中心重构):此前位于 `role/worker/` 下(该目录已消失)
3896
- *
3897
- * 落点 = `trajectory/delegate/`。判据(判读表 §3.1):它是**轨迹 × 轨迹**那种合法形态 ——
3898
- * 主语是「主轨迹把活交给子 agent」,既不是某一条轨迹的切面,也不是容器宪法。
3899
- * ⚠️ 它**不属于** `policy/`:policy 管"这条轨迹允许什么",本模块管"一条轨迹怎么把活派出去"。
3900
- */
3901
- /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
3902
- function splitModel(model) {
3903
- const idx = model.indexOf("/");
3904
- if (idx < 0) return {
3905
- provider: void 0,
3906
- model
3907
- };
3908
- return {
3909
- provider: model.slice(0, idx),
3910
- model: model.slice(idx + 1)
3911
- };
3912
- }
3913
- /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
3914
- function requireWhitelistedModel(model, models) {
3915
- if (!models.includes(model)) throw new Error(`cheap-worker: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "cheap-worker.models" with one of: ${models.join(", ")}`);
3916
- }
3917
- /**
3918
- * providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
3919
- * 返回 `null` = 全部可用(调用方照常继续)。
3920
- *
3921
- * 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
3922
- * **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
3923
- * 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
3924
- * 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
3925
- * 而**前台路径的 `diagnostic` 是空的** ⇒ 调用方(LLM)拿到 `stopReason:"error"` 却**不知道为什么**,
3926
- * 也就无法修正自己的配置 —— 本条把「沉默的失败」变成「可执行的错误」。
3927
- *
3928
- * 数据来源(宿主 `LlmRuntime`):`listProviders()` = **有 adapter 的路由**;
3929
- * `listConfigurableProviders()` = **已声明但未必激活**的路由(含激活用的 settings 命名空间)。
3930
- *
3931
- * 判据边界:`model` 串里**没有 `/`** 时取不到 provider(沿用 agent 默认路由)⇒ **不判定**(跳过)。
3932
- */
3933
- function providerAvailabilityError(models, facts) {
3934
- const registered = new Set(facts.registered.map((r) => r.id));
3935
- const requested = models.map((m) => ({
3936
- raw: m,
3937
- provider: splitModel(m).provider
3938
- })).filter((e) => typeof e.provider === "string" && e.provider !== "");
3939
- const missing = [...new Set(requested.filter((e) => !registered.has(e.provider)).map((e) => e.provider))];
3940
- if (missing.length === 0) return null;
3941
- const list = (items, cap = 12) => items.length === 0 ? " (none)" : items.slice(0, cap).map((s) => ` - ${s}`).join("\n") + (items.length > cap ? `\n … and ${items.length - cap} more` : "");
3942
- const available = facts.registered.map((r) => r.name ? `${r.id} (${r.name})` : r.id).sort();
3943
- const declared = (facts.declared ?? []).filter((d) => missing.includes(d.provider));
3944
- const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
3945
- const missingList = missing.map((p) => `"${p}"`).join(", ");
3946
- return [
3947
- `cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
3948
- "",
3949
- " Requested model(s) :",
3950
- ...requested.map((e) => ` ${e.raw}`),
3951
- ` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
3952
- "",
3953
- " Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
3954
- " provider adapters are registered per machine by the host. An entry that",
3955
- " works on one machine can fail on another.",
3956
- "",
3957
- " Available on this machine (registered adapters):",
3958
- list(available),
3959
- "",
3960
- " Declared but NOT active here (activate via settings, then retry):",
3961
- list(declared.map((d) => `${d.provider}${d.displayName ? ` (${d.displayName})` : ""}${d.settingsNs ? ` — settings namespace "${d.settingsNs}"` : ""}`)),
3962
- "",
3963
- " Fix (pick one, then retry):",
3964
- ` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
3965
- " (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
3966
- ` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
3967
- " or the settings namespace listed above).",
3968
- "",
3969
- " Do not retry unchanged — the same call fails identically."
3970
- ].join("\n");
3971
- }
3972
- /**
3973
- * cheap-worker 规模化使用指引(guide 子命令输出)。
3974
- *
3975
- * 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
3976
- * 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
3977
- */
3978
- const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
3979
-
3980
- ## What it is
3981
- One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
3982
- service, awaits them, and returns their final text. There is no loop, no completion code, no progress
3983
- file, and no unattended/background run.
3984
-
3985
- | | single | parallel (jobs) |
3986
- |---|---|---|
3987
- | Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
3988
- | Behaviour | one child runs the task once | all children start at once, results keep your order |
3989
- | Cap | — | **10 (fixed, hardcoded)** |
3990
- | Model | CCC config (single source) | same |
3991
-
3992
- ## ⚠️ Before using: load eap and design the plan
3993
- Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
3994
-
3995
- ### 1. Task decomposition (E↑ Explicit)
3996
- - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
3997
- - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
3998
-
3999
- ### 2. Prompt design (cheap-worker's task parameter)
4000
- - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
4001
- - the child does NOT share your conversation — restate everything it needs in task
4002
- - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
4003
- output a JSON array (fields id/conclusion/evidence), do not modify the original file"
4004
- - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
4005
- the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
4006
-
4007
- ### 3. Model (CCC-configured, not selectable per call)
4008
- - The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
4009
- (there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
4010
- - The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
4011
- - A child's own recursive subagents inherit the child's model automatically (DSH native)
4012
-
4013
- ### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
4014
- - The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
4015
- **adapters are registered per machine by the host** (profile config / settings namespace). An entry that
4016
- works on one machine can fail on another with: "no adapter registered for provider <p>".
4017
- - Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
4018
- **on this machine**; if it is missing it reports the requested model, where it is configured, **the
4019
- providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
4020
- exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
4021
- - ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
4022
- (and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
4023
- machine. **Do not retry the same call unchanged** — it fails identically.
4024
-
4025
- ### 4. Parallel strategy
4026
- - Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
4027
- **in the order you passed them** (label is only a display name).
4028
- - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
4029
-
4030
- ## Completion criteria
4031
- - The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
4032
- (and "output" for whatever partial text was produced).
4033
- `;
4034
- //#endregion
4035
- //#region src/trajectory/session.ts
4036
- /** `provider/model` → agentOptions(由 delegate/core 提供,保持单一实现) */
4037
- const trajectoryAgents = /* @__PURE__ */ new Map();
4038
- /** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
4039
- function getTrajectoryAgent(sessionId) {
4040
- return trajectoryAgents.get(sessionId);
4041
- }
4042
- /** 该会话当前绑定的 轨迹约束面(不是 轨迹约束 → null)。**守门与提示词的唯一查询入口。** */
4043
- function trajectoryConstraintFor(root, sessionId) {
4044
- if (sessionId === void 0) return null;
4045
- return trajectoryConstraintForSession(root, sessionId, trajectoryDirNameForSession(sessionId));
4046
- }
4047
- function registerTrajectorySession(sessionId, trajectoryDirName, ccc, agent) {
4048
- trajectoryAgents.set(sessionId, agent);
4049
- registerTrajectorySession$1(sessionId, trajectoryDirName, ccc);
4050
- }
4051
- function unregisterTrajectorySession(sessionId) {
4052
- trajectoryAgents.delete(sessionId);
4053
- unregisterTrajectorySession$1(sessionId);
4054
- }
4055
- /**
4056
- * 该 轨迹约束会话的**工作台路径** = 它自己轨迹的 SESSION.md。
4057
- *
4058
- * 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`ensureTrajectorySession`);
4059
- * 新版**没有新建这个动作** —— 轨迹就是工作台,`trajectory-constraint.json` 与 `SESSION.md` 同处一个目录。
4060
- * ⇒ 语义变成纯粹的"查询:我自己的身体在哪"。
4061
- *
4062
- * @returns SESSION.md 绝对路径;不是 轨迹约束 / 文件不在 → null
4063
- */
4064
- function trajectoryWorkspaceMdPath(root, sessionId) {
4065
- if (sessionId === void 0) return null;
4066
- const dirName = trajectoryDirNameForSession(sessionId);
4067
- if (dirName === null) return null;
4068
- const md = join(sessionsRoot(root), dirName, "SESSION.md");
4069
- return existsSync(md) ? md : null;
4070
- }
4071
- /**
4072
- * 系统提示词中的**工作台纪律块**(S142 用户:只给路径 LLM 不会主动用)。
4073
- *
4074
- * SESSION = 工作台 —— 轨迹约束与主舱同一套机制(**零特调**,用户拍板):
4075
- * SESSION.md 是持久记忆载体、rebuild 自动从本 SESSION 续接。
4076
- * 注入 agent 系统提示词(用户对话面不可见)。
4077
- *
4078
- * v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具** —— 旧文案写死 "with write/edit",
4079
- * 而某个 轨迹约束的工具白名单里根本没有 write/edit ⇒ 指令指向它没有的工具
4080
- * (注入纪律与能力面矛盾,模型只能猜)。现文案改为「用本轨迹被授权的写入通道」。
4081
- */
4082
- function workspaceCognitiveTrajectoryLine(mdPath) {
4083
- return [
4084
- "── Serenity Session Workspace ──",
4085
- "You ARE a cognitive trajectory: this workspace SESSION is your own persistent body.",
4086
- ` SESSION.md: ${mdPath}`,
4087
- "",
4088
- "Rules of the workspace:",
4089
- " 1. This SESSION.md is your persistent memory carrier (like the main cabin SESSION) — ",
4090
- " read it first when context feels thin or work spans turns.",
4091
- " 2. Record key decisions/progress/unresolved items INTO this SESSION.md using the write channel",
4092
- " your trajectory is actually granted (use whatever write tool or MSM you have — never assume",
4093
- " a specific write tool exists; if none is granted, report that instead of pretending to write).",
4094
- " Sections: 身份与边界 / 权威锚点 / 工作纪律 / 决策 / 日志 / 当前状态 / 待 owner 裁决.",
4095
- " Keep entries at milestone level (one line + evidence pointer); execution detail",
4096
- " belongs in commit messages. Keep it current — it is what a rebuild resumes from.",
4097
- " 3. When context pressure is high, run cognitive_trajectory rebuild — it auto-resumes from THIS SESSION.md.",
4098
- " 4. Never expose this internal path in user-facing replies (chat stays clean).",
4099
- "── ──"
4100
- ].join("\n");
4101
- }
4102
- /** 已挂工作台提示词段的 agent(幂等:同一 agent 只注册一次,避免 duplicate section) */
4103
- const workspaceSectionAgents = /* @__PURE__ */ new WeakSet();
4104
- /**
4105
- * 把**工作台纪律块**挂到 agent 的系统提示词段(单一注入点)。
4106
- *
4107
- * 用户原话(S142,2026-09-08):"微信桥的注入机制,每个用户消息都会注入,轨迹约束本身也会注入,
4108
- * 这样就重复,能否微信桥情况下,注入内容直接取 轨迹约束的,这样不用配两遍"。
4109
- *
4110
- * 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity-轨迹约束-workspace`,
4111
- * 动态 `text()` 每轮按 scope 读当前活跃 mdPath)。
4112
- *
4113
- * @returns true = 段已在位;false = 该 agent 挂不上(调用方降级为 question 前缀注入)
4114
- */
4115
- function ensureWorkspacePromptSection(agent, scope, root) {
4116
- if (!agent || typeof agent !== "object") return false;
4117
- if (workspaceSectionAgents.has(agent)) return true;
4118
- const api = agent.ctx?.systemPrompt;
4119
- if (typeof api?.section !== "function") return false;
4120
- try {
4121
- api.section({
4122
- name: "serenity-轨迹约束-workspace",
4123
- order: -55,
4124
- text: () => {
4125
- const mdPath = trajectoryWorkspaceMdPath(root, scope);
4126
- return mdPath ? workspaceCognitiveTrajectoryLine(mdPath) : "";
4127
- }
4128
- });
4129
- workspaceSectionAgents.add(agent);
4130
- return true;
4131
- } catch (err) {
4132
- console.warn(`[serenity-hooks] 轨迹约束 工作台提示词段注册失败(回退 question 注入): ${String(err?.message ?? err)}`);
4133
- return false;
4134
- }
4135
- }
4136
- /** 轨迹约束 agent 挂载的 DSH preset(平台工具面由 preset 决定;白名单再按轨迹过滤) */
4137
- const TRAJECTORY_PRESET = "standard";
4138
- /**
4139
- * 创建(或复用)一个 轨迹约束会话。
4140
- *
4141
- * **固定 sessionId 的 resume-or-create**(v1.27.2):指定 sessionId 且磁盘已有持久化 log →
4142
- * `ctx.agents.resume`(DSH 持久化语义:sessionId 即身份,历史 + turn 续号延续);
4143
- * 无 log(首次)→ `ctx.agents.create`。不指定 → 随机 id 恒 create(调试页路径)。
4144
- *
4145
- * @param profile 该 轨迹约束的约束面(来自 `trajectory-constraint.json`)
4146
- * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现"轨迹 ↔ 会话"长期映射)
4147
- */
4148
- async function createTrajectoryAgent(ctx, root, profile, defaultModel, sessionId) {
4149
- if (!ctx.agents) throw new Error("trajectory constraint: ctx.agents unavailable");
4150
- const model = profile.model?.trim() || defaultModel || "";
4151
- const id = sessionId ?? `轨迹约束-${profile.code}-${randomUUID()}`;
4152
- const setup = async (agentCtx) => {
4153
- try {
4154
- await agentCtx.get("agentPresets")?.mount?.(agentCtx, TRAJECTORY_PRESET);
4155
- } catch {}
4156
- };
4157
- const handle = await createOrResumeAgent(ctx, id, root, model, setup, sessionId !== void 0);
4158
- const agent = handle.agent;
4159
- let readerPath = profile.systemPromptFile?.trim();
4160
- let reader = createProfilePromptReader(root, profile);
4161
- const livePrompt = () => {
4162
- let live = profile;
4163
- try {
4164
- const fresh = readTrajectoryConstraintAt(join(sessionsRoot(root), profile.dirName), root);
4165
- if (fresh) live = fresh;
4166
- } catch {}
4167
- const wantPath = live.systemPromptFile?.trim();
4168
- if (wantPath !== readerPath) {
4169
- reader = createProfilePromptReader(root, live);
4170
- readerPath = wantPath;
4171
- }
4172
- return reader();
4173
- };
4174
- livePrompt();
4175
- try {
4176
- agent.ctx.systemPrompt.section({
4177
- name: "serenity-轨迹约束",
4178
- order: -60,
4179
- text: () => {
4180
- const legacy = hasTrajectoryPrompt(root, profile.dirName) ? "" : livePrompt();
4181
- return [buildTrajectoryBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
4182
- }
4183
- });
4184
- } catch (err) {
4185
- console.warn(`[serenity-hooks] 轨迹约束 系统提示词注册失败: ${String(err?.message ?? err)}`);
4186
- }
4187
- if (agent?.session) try {
4188
- registerCognitiveTrajectorySkillSection(agent, root);
4189
- } catch (err) {
4190
- console.warn(`[serenity-hooks] 轨迹约束轨迹 skill 注入注册失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
4191
- }
4192
- if (trajectoryWorkspaceMdPath(root, id) !== null) ensureWorkspacePromptSection(agent, id, root);
4193
- registerTrajectorySession(id, profile.dirName, root, agent);
4194
- return {
4195
- handle,
4196
- agent,
4197
- sessionId: id,
4198
- resumed: handle.resumed
4199
- };
4200
- }
4201
- /** 取 live agent(DSH 文档:`ctx.agents.get(id)` 返回 bare Agent;会话 live 时 resume/create 均不可用) */
4202
- function getLiveAgent(ctx, id) {
4203
- try {
4204
- return hostAgents(ctx)?.get?.(id);
4205
- } catch {
4206
- return;
4207
- }
4208
- }
4209
- /** live 复用返回(resumed:true 历史延续;dispose 空操作——live agent 不归本模块所有,不能拆) */
4210
- function liveReuseRef(live, setup) {
4211
- return setup(live.ctx).catch(() => {}).then(() => ({
4212
- agent: live,
4213
- dispose: async () => {},
4214
- resumed: true
4215
- }));
4216
- }
4217
- /**
4218
- * resume-or-create 分派:固定 id → 优先 resume(持久化历史延续),失败降级 create;
4219
- * 随机 id(无固定 sessionId)→ 恒 create。
4220
- *
4221
- * **v1.27.3 修复(微信桥"重启后不响应"根因):live 会话优先复用**——重启后 DSH 把
4222
- * 关闭时仍 live 的持久化会话恢复为 live。此时 resume 报
4223
- * "cannot prepare session X while it is live"、create 报 "session X already exists"
4224
- * ⇒ 唯一可行路径 = `ctx.agents.get(id)` 取 live agent 直接续用。
4225
- */
4226
- async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
4227
- if (!fixedId) return {
4228
- ...await ctx.agents.create({
4229
- sessionId: id,
4230
- meta: {
4231
- cwd: root,
4232
- agentPreset: TRAJECTORY_PRESET
4233
- },
4234
- setup,
4235
- ...model ? { agentOptions: splitModel(model) } : {}
4236
- }),
4237
- resumed: false
4238
- };
4239
- const liveAgent = getLiveAgent(ctx, id);
4240
- if (liveAgent) return liveReuseRef(liveAgent, setup);
4241
- const agentsWithResume = ctx.agents;
4242
- if (typeof agentsWithResume.resume !== "function") return {
4243
- ...await ctx.agents.create({
4244
- sessionId: id,
4245
- meta: {
4246
- cwd: root,
4247
- agentPreset: TRAJECTORY_PRESET
4248
- },
4249
- setup,
4250
- ...model ? { agentOptions: splitModel(model) } : {}
4251
- }),
4252
- resumed: false
4253
- };
4254
- try {
4255
- return {
4256
- ...await agentsWithResume.resume({
4257
- resumeSessionId: id,
4258
- setup,
4259
- ...model ? { agentOptions: splitModel(model) } : {}
4260
- }),
4261
- resumed: true
4262
- };
4263
- } catch (err) {
4264
- const liveNow = getLiveAgent(ctx, id);
4265
- if (liveNow) return liveReuseRef(liveNow, setup);
4266
- const msg = err instanceof Error ? err.message : String(err);
4267
- const stack = err instanceof Error ? err.stack ?? "" : "";
4268
- console.log(`[serenity-hooks] 轨迹约束 resume 失败降级 create (id=${id}): ${msg}`);
4269
- if (stack) console.log(`[serenity-hooks] 轨迹约束 resume stack:\n${stack.slice(0, 1200)}`);
4270
- return {
4271
- ...await ctx.agents.create({
4272
- sessionId: id,
4273
- meta: {
4274
- cwd: root,
4275
- agentPreset: TRAJECTORY_PRESET
4276
- },
4277
- setup,
4278
- ...model ? { agentOptions: splitModel(model) } : {}
4279
- }),
4280
- resumed: false
4281
- };
4282
- }
4283
- }
4284
- /** 会话事件读取(兼容 `snapshotEvents()` 与测试替身的 `.events`) */
4285
- function sessionEvents(session) {
4286
- const s = session;
4287
- if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
4288
- return s.events ?? [];
4289
- }
4290
- /** 读会话最后一个 assistant/message 文本 */
4291
- function lastAssistantText$1(agent) {
4292
- const events = sessionEvents(agent.session);
4293
- for (let i = events.length - 1; i >= 0; i--) {
4294
- const e = events[i];
4295
- if (e && e.type === "assistant/message") {
4296
- const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
4297
- if (text) return text;
4298
- }
4299
- }
4300
- return "";
4301
- }
4302
- /** 本轮等待上限(毫秒):超时即判「本轮无产出」——**绝不**把上一轮的旧答案当本轮答案 */
4303
- const ASK_TURN_TIMEOUT_MS = 18e4;
4304
- /** 会话里 `assistant/message` 的条数(判「本轮是否真的产出了回答」) */
4305
- function countAssistantMessages(session) {
4306
- let n = 0;
4307
- for (const e of sessionEvents(session)) if (e?.type === "assistant/message") n += 1;
4308
- return n;
4309
- }
4310
- /**
4311
- * 等**本轮**产出一条 assistant 消息。
4312
- *
4313
- * ## 为什么不能用 `waitAgentIdle` 单独当判据(2026-10-19 复读缺陷)
4314
- *
4315
- * `waitAgentIdle` 的首行是 `if (agent.status === 'idle') finish()` —— 而 `followup()`
4316
- * 是**异步投递**:刚投出去的这一刻 agent 状态**仍是 idle** ⇒ 等待**立刻结算** ⇒
4317
- * `lastAssistantText()` 读到的是**上一轮的** `assistant/message` ⇒ 桥把它当"本轮答案"转发。
4318
- * 若 CCC 又开了 `fallbackOnNoSend`,同一条旧答案会被反复重发(实测三次间隔 300~400ms、逐字相同)。
4319
- *
4320
- * ## 判据(`idle` 只是"可以开始看"的信号,不是成功信号)
4321
- *
4322
- * ① 期望值 = 调用前 `assistant/message` 的**条数**;
4323
- * ② 成功 ⟺ 该计数**增长**;
4324
- * ③ 超时或有界轮询耗尽 ⇒ `false`(调用方据此给空答案,兜底不触发)。
4325
- */
4326
- async function waitForTurnAssistant(ctx, agent, beforeAssistantCount, opts = {}) {
4327
- const timeoutMs = opts.timeoutMs ?? ASK_TURN_TIMEOUT_MS;
4328
- const grew = () => countAssistantMessages(agent.session) > beforeAssistantCount;
4329
- if (await Promise.race([waitAgentIdle(ctx, agent).then(() => true), new Promise((resolve) => {
4330
- setTimeout(() => resolve(false), timeoutMs);
4331
- })])) return grew();
4332
- return grew();
4333
- }
4334
- /**
4335
- * 提问一轮:followup → 等**本轮**回答 → 读答案。
4336
- *
4337
- * @param options.timeoutMs 本轮等待上限(测试注入;生产用缺省)
4338
- */
4339
- async function askTrajectory(ctx, agent, question, options) {
4340
- const beforeAssistant = countAssistantMessages(agent.session);
4341
- agent.followup(createUserMessage({
4342
- content: [{
4343
- type: "text",
4344
- text: question
4345
- }],
4346
- source: PLUGIN_SOURCE
4347
- }));
4348
- return {
4349
- answer: await waitForTurnAssistant(ctx, agent, beforeAssistant, { timeoutMs: options?.timeoutMs }) ? lastAssistantText$1(agent) : "",
4350
- sessionId: String(agent.session.id ?? "")
4351
- };
4352
- }
4353
- /**
4354
- * 轨迹约束会话的轨迹纪律参与判定:**不是 轨迹约束恒 true**(正常参与);
4355
- * 轨迹约束 按 `trajectory-constraint.json` 的 `cognitiveTrajectory` 子集决定。
4356
- *
4357
- * 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `trajectoryConstraintForSession` 回落持久绑定;
4358
- * 两者都不中 ⇒ 判"不是 轨迹约束" ⇒ 恒 true(正常参与)。**fail-open 是有意的**:
4359
- * 纪律参与项的失败不该让会话失去 keeper/rebuild(那是能力倒退,比多跑一次更糟)。
4360
- */
4361
- function trajectoryConstraintParticipates(root, sessionId, key) {
4362
- if (sessionId === void 0) return true;
4363
- const profile = trajectoryConstraintFor(root, sessionId);
4364
- if (profile === null) return true;
4365
- return constraintCognitiveTrajectory(profile)[key];
4366
- }
4367
- /**
4368
- * `msm` 工具的 轨迹约束门控:非 轨迹约束恒放行;轨迹约束会话——
4369
- * exec 非白名单 MSM 拒绝(**不列名单**)、register/deregister 必拒、
4370
- * list 白名单过滤、check/guide/ccc-config 只读放行。
4371
- */
4372
- function trajectoryConstraintMsmGate(root, sessionId, action, name) {
4373
- if (sessionId === void 0) return {};
4374
- const profile = trajectoryConstraintFor(root, sessionId);
4375
- if (profile === null) return {};
4376
- if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in trajectory-constraint sessions" };
4377
- if (action === "exec") {
4378
- if (!name || !profile.msms.includes(name)) return { reject: "MSM not allowed" };
4379
- return {};
4380
- }
4381
- if (action === "list") return { whitelist: profileMsmWhitelist(profile) };
4382
- return {};
4383
- }
4384
- //#endregion
4385
1902
  //#region src/tools/msm/index.ts
4386
1903
  /**
4387
1904
  * tools/msm/index.ts — msm 工具(v1.30 重构:单入口执行,list/exec 合一)
@@ -12443,6 +9960,107 @@ ${renderCroSampleSnapshot()}
12443
9960
  **吃一份快照,吐一行决策。先自测,绿了再改名。**
12444
9961
  `;
12445
9962
  //#endregion
9963
+ //#region src/trajectory/resident.ts
9964
+ /**
9965
+ * trajectory/resident.ts — **常驻名单**(D173 的兑现口,`cognitive_trajectory resident` 的实现)
9966
+ *
9967
+ * ## 这条能力的判据(owner 2026-10-20 原话,逐字)
9968
+ *
9969
+ * > 「常驻的意思就是**自己声明为自己欢迎别人来麻烦自己**,和永不休眠没啥关系,
9970
+ * > **trajectory never ends 悖论的一种实现**」
9971
+ *
9972
+ * ⇒ **常驻不是资源状态**。它不是"永不休眠"、不是"常驻内存"、不是"不许销毁我的会话"。
9973
+ * 一条轨迹**永远存在**(没有生命周期),常驻与否都不改变这一点。
9974
+ *
9975
+ * ⇒ **它管的是「被找到」,不是「不被销毁」。**
9976
+ * 两件事混为一谈,就会去动调度器的载体生命周期 —— 那正是本轨反复踩的"名字撒谎"。
9977
+ *
9978
+ * ## 因此本模块的三条硬边界
9979
+ *
9980
+ * 1. **纯读**:不写任何文件、不碰注册表、不碰 `wake/scheduler.ts`。
9981
+ * 2. **不进系统提示词**(owner 裁决 Q2:说明类信息按需取,不占每轮 token)。
9982
+ * 3. **只报写了 `serves` 的轨迹** —— 只写半个(有 `resident` 没 `serves`)等于
9983
+ * "把自己挂上去却不说能干什么",按手册的判据那比不写更坏,所以**不列出来**,
9984
+ * 但要**单独数出**来,让写的人知道自己的写法没生效。
9985
+ *
9986
+ * ## 归属(D23)
9987
+ * 机制在 ACC(怎么找、按什么顺序、列什么字段);内容在 CCC(`serves` 那句话写什么)。
9988
+ */
9989
+ /**
9990
+ * 列出全部**可被找到**的轨迹(按编码排序,确定性)。
9991
+ *
9992
+ * ⚠️ 读盘失败 / 坏 JSON 一律**跳过该条**(不抛):一份坏的声明不该让整张名单拿不到。
9993
+ *
9994
+ * @param root CCC 根
9995
+ */
9996
+ function collectResident(root) {
9997
+ const entries = [];
9998
+ const incomplete = [];
9999
+ let names = [];
10000
+ try {
10001
+ names = readdirSync(sessionsRoot(root), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name);
10002
+ } catch {
10003
+ return {
10004
+ entries,
10005
+ incomplete
10006
+ };
10007
+ }
10008
+ for (const dirName of names) {
10009
+ let profile;
10010
+ try {
10011
+ profile = readTrajectoryConstraintAt(join(sessionsRoot(root), dirName), root);
10012
+ } catch {
10013
+ continue;
10014
+ }
10015
+ if (!profile || profile.resident === void 0) continue;
10016
+ const serves = profile.resident.serves;
10017
+ if (serves === void 0 || serves === "") {
10018
+ incomplete.push(profile.code);
10019
+ continue;
10020
+ }
10021
+ entries.push({
10022
+ code: profile.code,
10023
+ dirName: profile.dirName,
10024
+ serves,
10025
+ ...profile.model !== void 0 ? { model: profile.model } : {}
10026
+ });
10027
+ }
10028
+ entries.sort((a, b) => a.code < b.code ? -1 : a.code > b.code ? 1 : 0);
10029
+ incomplete.sort();
10030
+ return {
10031
+ entries,
10032
+ incomplete
10033
+ };
10034
+ }
10035
+ /**
10036
+ * 渲染成给模型读的文本(`cognitive_trajectory resident` 的返回值)。
10037
+ *
10038
+ * 🔴 **空名单也是有效结果**,且必须说清"这意味着什么" —— 否则读者会以为工具坏了。
10039
+ */
10040
+ function listResident(root) {
10041
+ const { entries, incomplete } = collectResident(root);
10042
+ const lines = ["═══ 常驻轨迹(自己声明「欢迎别人来麻烦我」)═══", ""];
10043
+ if (entries.length === 0) {
10044
+ lines.push(" (本 CCC 暂无轨迹声明常驻)");
10045
+ lines.push(" ⇒ 这不是错误:常驻是**自愿的自我声明**,没写就是\"不参与被找\"。");
10046
+ } else for (const e of entries) {
10047
+ const model = e.model !== void 0 ? ` [model: ${e.model}]` : "";
10048
+ lines.push(` · ${e.code}${model}`);
10049
+ lines.push(` ${e.serves}`);
10050
+ }
10051
+ if (incomplete.length > 0) {
10052
+ lines.push("");
10053
+ lines.push(` ⚠️ 写了 resident 但**没写 serves**(不会被列出):${incomplete.join("、")}`);
10054
+ lines.push(" 只写半个 = 把自己挂上去却不说能干什么 —— 别人会来问,然后发现问错了人。");
10055
+ }
10056
+ lines.push("");
10057
+ lines.push("── 怎么让自己出现在这里 ──");
10058
+ lines.push(" 在你这条轨迹目录的 trajectory.json 里写:");
10059
+ lines.push(" { \"resident\": { \"serves\": \"一句话说清我擅长什么、能帮上什么忙\" } }");
10060
+ lines.push(" ⚠️ 只写这一个字段**不影响任何默认行为**(D183);其余 key 全部可选。");
10061
+ return lines.join("\n");
10062
+ }
10063
+ //#endregion
12446
10064
  //#region src/tools/cognitive-trajectory.ts
12447
10065
  /**
12448
10066
  * trajectory.ts — `cognitive_trajectory` 真实 DSH 工具定义(defineTool)
@@ -12640,7 +10258,8 @@ function createCognitiveTrajectoryTool(ctx) {
12640
10258
  "DELIVERY (sends one message to ANOTHER cognitive trajectory; your binding is unchanged):",
12641
10259
  "send-now (deliver RIGHT NOW; the target needs no prior setup — if it has no live session, one is created and bound automatically) /",
12642
10260
  "send-later (one future instant + one message; fire-and-forget, and it does NOT auto-create a target).",
12643
- "REFERENCE: cro-guide (no parameters: prints the CRO programming guide, including a ready-to-use sample snapshot)"
10261
+ "REFERENCE: cro-guide (no parameters: prints the CRO programming guide, including a ready-to-use sample snapshot) /",
10262
+ "resident (READ-ONLY: the roster of trajectories that declared themselves RESIDENT — \"I am here, this is what I am good at, come bother me\". It lists each code with its own `serves` sentence, plus any code that wrote `resident` without `serves` (half a declaration is worse than none: people ask, then find they asked the wrong one). ⚠️ Residency is NOT a resource state — it does not mean \"never sleeps\" or \"never destroyed\"; a trajectory permanently exists either way. It governs BEING FOUND, not being kept alive, and it never touches the scheduler)."
12644
10263
  ].join(" ")
12645
10264
  },
12646
10265
  name: {
@@ -12860,6 +10479,21 @@ function createCognitiveTrajectoryTool(ctx) {
12860
10479
  };
12861
10480
  }
12862
10481
  case "cro-guide": return CRO_GUIDE;
10482
+ case "resident":
10483
+ /**
10484
+ * 常驻名单(D173 兑现,owner 2026-10-20)。
10485
+ *
10486
+ * 🔴 **这个动作的存在本身就是一条判据**:v1.77.1 的手册曾承诺"通过本工具的
10487
+ * 常驻名单动作按需查询",而**那个动作当时并不存在**(D179(a),S196 踩到)。
10488
+ * ⇒ 现在它真的存在了。
10489
+ *
10490
+ * 🔴 **纯读**(不碰 fs 写、不碰注册表、不碰调度器):D173 的判据是
10491
+ * 「常驻」管的是**被找到**,不是**不被销毁** —— 两件事混为一谈就会去动
10492
+ * `wake/scheduler.ts` 的载体生命周期,那正是本轨反复踩的"名字撒谎"。
10493
+ *
10494
+ * 🔴 **不进系统提示词**(owner 裁决 Q2:说明类信息按需取,不占每轮 token)。
10495
+ */
10496
+ return listResident(root);
12863
10497
  default: throw new Error(`Unknown action: ${args.action}`);
12864
10498
  }
12865
10499
  }
@@ -12986,26 +10620,33 @@ becomes a 轨迹约束; delete it and it goes back to being an ordinary trajecto
12986
10620
 
12987
10621
  ── ${TRAJECTORY_CONSTRAINT_FILENAME} schema(限制面部分)──
12988
10622
  {
12989
- "model": "provider/model", // optional; falls back to cheap-worker.defaultModel
12990
- "tools": ["read","grep","msm",...], // non-MSM toolbar whitelist — anything not listed is REMOVED from the schema
12991
- "msms": ["msm-a","msm-b"], // MSM whitelist — msm exec may only run these; register/deregister always refused
12992
- "cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, // defaults TRUE (see below)
10623
+ "model": "provider/model", // optional; **不写 = 用会话当前模型**(不回退 cheap-worker;每轮生效)
10624
+ "tools": ["read","grep","msm",...], // optional; **不写 = 不收窄**。显式 [] = 全隐藏(除 skill)
10625
+ "msms": ["msm-a","msm-b"], // optional; **不写 = 不收窄**。register/deregister 恒拒
10626
+ "cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, // 缺省 TRUE(见下)
10627
+ "resident": { "serves": "..." }, // optional; 对外用途自述(见 trajectory-self 章)
12993
10628
  "cro": { "host": "<machine name>" } // optional: which machine may run this trajectory's CRO program
12994
10629
  }
12995
10630
 
10631
+ 🔴 **所有 key 都是可选的:不写 = 不设定 = 不影响默认行为**(owner 2026-10-20 裁决)。
10632
+ ⛔ **"没写" ≠ "写了空数组"**:\`"tools": []\` 是"我一个都不给"(除 \`skill\` 外全隐藏);
10633
+ **不写 \`tools\`** 是"不收窄"。这两者在旧版被混成同一个意思,害人把自己锁死过。
10634
+
12996
10635
  ⚠️ 提示词**不在这里**(新形态):写 \`PROMPT.md\` 即可,**所有轨迹**都适用。
12997
10636
  旧字段 \`systemPromptFile\` / \`systemPrompt\` 属**兼容期**形态,仍在读、但不再推荐;
12998
10637
  两者同时存在时 \`PROMPT.md\` 让位规则见 \`trajectory/prompt.ts\`(此处不重复)。
12999
10638
 
13000
- ⚠️ **只想声明常驻、不想限制自己**:只写 \`"resident"\` 字段、**不写 tools/msms**。
13001
- 空白名单 = **不收窄任何工具面**(见下方"两个默认")—— 但请注意你仍因此"成为轨迹约束",
13002
- 只是没有实际限制。
10639
+ ⚠️ **只想声明常驻、不想限制自己**:只写 \`"resident"\` 字段,**tools/msms 一个都不要写**。
10640
+ ⇒ 不收窄任何工具面(见下方"两个默认")。⛔ **不要**为了"空着"而写 \`"tools": []\` —— 那是全隐藏。
13003
10641
 
13004
- ── Two whitelists, both default-deny ──
13005
- - \`tools: []\` + non-empty \`msms\` = pure-MSM 轨迹约束 (the usual knowledge-QA shape)
13006
- - 空 \`tools\` + 空 \`msms\` = **无限制**(普通行为;常驻自述用的就是这一格)
10642
+ ── Two whitelists: UNSET means unrestricted, EMPTY means nothing ──
10643
+ - **字段缺省(没写)** = **不收窄**(这条轨迹只放别的设定,比如只写 resident)
10644
+ - \`"tools": []\`(显式空数组)= **全隐藏**,除恒可用的 \`skill\`
10645
+ - \`tools\` + non-empty \`msms\` = pure-MSM trajectory (the usual knowledge-QA shape)
13007
10646
  - non-empty \`msms\` ⇒ the \`msm\` tool is added automatically (the only entry point for MSMs)
13008
- - anything outside both lists is invisible in the schema, not merely rejected at call time
10647
+ - anything outside a **declared** whitelist is invisible in the schema, not merely rejected at call time
10648
+ 🔴 判据是**字段在不在**,不是**数组空不空**(D181)。旧版把两者混成"空 = 全拒",
10649
+ 于是「只想声明常驻」的人被静默锁死 —— 那是本仓实测踩过的坑(D179(b))。
13009
10650
 
13010
10651
  ── cognitiveTrajectory: why the defaults are TRUE ──
13011
10652
  The workspace of a 轨迹约束 is its own SESSION.md. Setting these to false would cut it off from
@@ -13046,7 +10687,7 @@ function validateTrajectoryConstraint(root) {
13046
10687
  return true;
13047
10688
  }
13048
10689
  })())) issues.push(`trajectory "${p.code}": no trajectory prompt (add ${TRAJECTORY_PROMPT_FILENAME} in the trajectory dir — recommended — or the legacy systemPromptFile/systemPrompt)`);
13049
- for (const m of p.msms) if (!registered.has(m)) issues.push(`trajectory "${p.code}": msms "${m}" is not registered in mech-registry.json`);
10690
+ for (const m of p.msms ?? []) if (!registered.has(m)) issues.push(`trajectory "${p.code}": msms "${m}" is not registered in mech-registry.json`);
13050
10691
  if (p.model && allowedModels && !allowedModels.has(p.model)) issues.push(`trajectory "${p.code}": model "${p.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
13051
10692
  }
13052
10693
  return {
@@ -13062,16 +10703,29 @@ function listTrajectoryConstraintSummary(root) {
13062
10703
  roles: [],
13063
10704
  note: `no 轨迹约束 trajectories in this CCC (no trajectory has ${TRAJECTORY_CONSTRAINT_FILENAME})`
13064
10705
  };
13065
- const items = profiles.map((p) => ({
13066
- name: p.code,
13067
- dirName: p.dirName,
13068
- model: p.model ?? "(cheap-worker default)",
13069
- msms: p.msms,
13070
- tools: p.tools,
13071
- cognitiveTrajectory: constraintCognitiveTrajectory(p),
13072
- promptSource: constraintSystemPromptSource(p, hasTrajectoryPrompt(root, p.dirName)),
13073
- ...p.systemPromptFile ? { systemPromptFile: p.systemPromptFile } : {}
13074
- }));
10706
+ const items = profiles.map((p) => {
10707
+ /**
10708
+ * 🔴 三个字段的读数必须**如实表达三态**(D179(c)+D181,S196 就是被这里的旧读数误导的)。
10709
+ *
10710
+ * | 文件里 | 打印 | 为什么不能印别的 |
10711
+ * |---|---|---|
10712
+ * | 没写 `model` | `'(unchanged)'` | 旧文案印 `'(cheap-worker default)'` ⇒ **暗示会回退到杂工模型**,而实际是**不动**。那是一次**空头承诺**:用户以为要被换模型,其实是误解 |
10713
+ * | 没写 `tools`/`msms` | `'(unrestricted)'` | 缺省 = **不收窄**(D181)。印 `[]` 会让人以为"我被全隐藏了" |
10714
+ * | 写了 `[]` | `'[]'` | 显式空数组 = **全隐藏**,与上一行**恰好相反**,必须能一眼分辨 |
10715
+ */
10716
+ const wl = (v, whenUnset) => v === void 0 ? whenUnset : v;
10717
+ return {
10718
+ name: p.code,
10719
+ dirName: p.dirName,
10720
+ model: p.model ?? "(unchanged)",
10721
+ msms: wl(p.msms, "(unrestricted)"),
10722
+ tools: wl(p.tools, "(unrestricted)"),
10723
+ ...p.resident !== void 0 ? { resident: p.resident.serves !== void 0 ? p.resident.serves : "(no serves)" } : {},
10724
+ cognitiveTrajectory: constraintCognitiveTrajectory(p),
10725
+ promptSource: constraintSystemPromptSource(p, hasTrajectoryPrompt(root, p.dirName)),
10726
+ ...p.systemPromptFile ? { systemPromptFile: p.systemPromptFile } : {}
10727
+ };
10728
+ });
13075
10729
  return {
13076
10730
  roles: items,
13077
10731
  count: items.length
@@ -13153,28 +10807,46 @@ const TRAJECTORY_SELF_GUIDE = [
13153
10807
  只写半个(有字段、没 serves)等于把自己挂上去却不说能干什么 —— 那比不写更坏,
13154
10808
  因为别人会来问,然后发现问错了人。
13155
10809
 
13156
- 谁能看见:通过 cognitive_trajectory 的常驻名单动作按需查询。
10810
+ 谁能看见:\`cognitive_trajectory\` 的 **\`resident\` 动作**(只读发现)。
13157
10811
  **它不进系统提示词**(不占每个请求的 token)—— 只在有人真的要找人时才读。
13158
- 这是 owner 2026-10-20 的裁决:说明类信息按需取,不常驻在上下文里。`,
10812
+ 这是 owner 2026-10-20 的裁决:说明类信息按需取,不常驻在上下文里。
10813
+
10814
+ 🔴 它**不碰任何默认行为**(D183):只写 resident 的轨迹,ACC 身份注入、首轮锚定、
10815
+ 自动恢复、compact 重注入 **全部照常**。常驻管的是**被找到**,不是**不被销毁**。`,
13159
10816
  "",
13160
- `── 想限制自己(白名单 / 模型 / 纪律参与项)──
10817
+ `── 🔴 所有 key 都是可选的:不写 = 不设定 = 不影响默认行为 ──
13161
10818
 
13162
- 写 \`trajectory.json\` 即成为**轨迹约束**(判据是**文件存在**,不是某个字段存在)。
13163
- 删掉它 ⇒ 当场退回普通轨迹。
10819
+ 这条是总规则(owner 2026-10-20 裁决)。下面逐字段说清"不写会怎样":
13164
10820
 
13165
- ⚠️ 一个必须知道的连带后果:**写 trajectory.json 会把我变成受约束的轨迹**。
13166
- 所以如果你只想"描述自己"(章程、常驻),**不需要**建这个文件 ——
13167
- 章程走 PROMPT.md;常驻字段确实也在这个文件里,但**只写 resident、不写 tools/msms**
13168
- 时,白名单为空 = 不收窄任何工具面(见下一章"两个默认")。
10821
+ | 字段 | 不写它 | 生效时机 |
10822
+ |---|---|---|
10823
+ | \`model\` | 用会话当前的模型(**不会**回退到 cheap-worker) | **每轮**(改文件下一轮即生效) |
10824
+ | \`tools\` | **不收窄**工具面 | 每轮 |
10825
+ | \`msms\` | **不收窄** MSM 面 | 每轮 |
10826
+ | \`resident\` | 不出现在常驻名单里(不参与"被找") | 每轮 |
10827
+ | \`cognitiveTrajectory\` | 三项纪律全参与(true) | 每轮 |
10828
+ | \`systemPromptFile\` / \`systemPrompt\` | 用 ACC 的通用身份(**不替换**) | 保存即生效 |
10829
+
10830
+ 🔴 **"没写" 与 "写了空数组" 是两种意思**:
10831
+ \`"tools": []\`(显式空数组)= **我一个都不给** ⇒ 除 \`skill\` 外全部工具被隐藏。
10832
+ **不写 \`tools\`** = **不收窄** ⇒ 工具面完全不变。
10833
+ ⇒ 想只声明常驻?那就**只写 resident**,别的键一个都不要写。
10834
+
10835
+ 🔴 **写 \`trajectory.json\` 本身不改任何默认行为** —— 它只是"我把设定放这儿了"。
10836
+ 只有当某个 key **真的取代了默认**(目前唯一一个是自带章程 systemPrompt*)时,
10837
+ 这条轨迹才不再注入 ACC 身份。其余字段都不影响。`,
10838
+ "",
10839
+ `── 想限制自己(白名单 / 模型 / 纪律参与项)──
13169
10840
 
13170
10841
  schema、双白名单、纪律参与项的完整说明 → container_admin config docs trajectory-constraint`,
13171
10842
  "",
13172
10843
  `── 写完之后怎么验(别只看文档,要能机械判定)──
13173
10844
 
13174
10845
  章程生效了吗 改 PROMPT.md 后**在同一会话里**看下一轮的系统提示词段是否变化(保存即生效)
13175
- 我是轨迹约束吗 container_admin config list —— 我的编码出现在列表里即成立
13176
- 配置合法吗 container_admin config validate —— msms 均已注册 / model 在可用表里 / 提示词非空
13177
- 常驻被看见了吗 cognitive_trajectory 的常驻名单动作 —— 我出现在名单里,且 serves 可读
10846
+ 我声明了什么 container_admin config list —— 逐轨迹列出 model/tools/msms 的**生效值**;
10847
+ 没写的字段印 \`(unchanged)\` / \`(unrestricted)\`,不是伪装成某个默认值
10848
+ 配置合法吗 container_admin config validate —— msms 均已注册 / model 在可用表里
10849
+ 常驻被看见了吗 cognitive_trajectory action=resident —— 我出现在名单里,且 serves 可读
13178
10850
 
13179
10851
  ⚠️ 以上四项都是**读取当前真实状态**,不是"保存成功"的回执。保存成功 ≠ 生效(本仓反复踩过)。`
13180
10852
  ].join("\n");
@@ -13739,11 +11411,12 @@ function decideGuard(input) {
13739
11411
  if (input.trajectoryConstraint !== void 0 && input.trajectoryConstraint !== null) {
13740
11412
  const profile = input.trajectoryConstraint;
13741
11413
  if (toolName === "skill") return { kind: "allow" };
13742
- if (profile.msms.includes(toolName)) return {
11414
+ if ((profile.msms ?? []).includes(toolName)) return {
13743
11415
  deny: `"${toolName}" is a registered MSM, not a direct tool in this trajectory — call it through the msm tool: msm("${toolName}", ["<args>"]) (pass "--help" or inspect=true for usage)`,
13744
11416
  kind: "deny"
13745
11417
  };
13746
- if (!profileToolWhitelist(profile).has(toolName)) return {
11418
+ const whitelist = profileToolWhitelist(profile);
11419
+ if (whitelist !== null && !whitelist.has(toolName)) return {
13747
11420
  deny: "tool not allowed in this 轨迹约束 trajectory",
13748
11421
  kind: "deny"
13749
11422
  };
@@ -14069,6 +11742,15 @@ function syncTrajectoryConstraintToolVisibility(agent, root) {
14069
11742
  return;
14070
11743
  }
14071
11744
  const allowed = profileToolWhitelist(profile);
11745
+ if (allowed === null) {
11746
+ if (existing !== void 0) {
11747
+ try {
11748
+ existing.dispose();
11749
+ } catch {}
11750
+ trajectoryConstraintRestrictions.delete(sessionId);
11751
+ }
11752
+ return;
11753
+ }
14072
11754
  const resident = REGISTERED_TOOLS.slice(0, REGISTERED_TOOLS.length - 3);
14073
11755
  const tiers = [REGISTERED_TOOLS, resident];
14074
11756
  for (const names of tiers) {
@@ -14366,7 +12048,7 @@ const DEFAULT_FAKE_INITIAL_MESSAGES = ["You are the operator agent of Serenity
14366
12048
  * 统一经 sessionEvents() 读取(snapshotEvents() 优先,.events 兜底)。
14367
12049
  */
14368
12050
  function hasUserMessageHistory(session) {
14369
- return sessionEvents$1(session).some((event) => {
12051
+ return sessionEvents(session).some((event) => {
14370
12052
  const e = event;
14371
12053
  if (e.type !== "user/message") return false;
14372
12054
  return e.data?.source?.kind === "user";
@@ -14511,7 +12193,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
14511
12193
  let boundary = -1;
14512
12194
  let signalCount = 0;
14513
12195
  let rounds = 0;
14514
- const events = sessionEvents$1(session);
12196
+ const events = sessionEvents(session);
14515
12197
  if (events.length > 0) for (const event of events) {
14516
12198
  const e = event;
14517
12199
  const seq = typeof e.seq === "number" ? e.seq : 0;
@@ -14550,7 +12232,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
14550
12232
  const loopSid = session.id;
14551
12233
  const loopCwd = session.header?.cwd ?? process.cwd();
14552
12234
  const loopRoot = cccRootForCwd(loopCwd);
14553
- if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && isTrajectoryConstrained(loopRoot, loopSid))) return {
12235
+ if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && replacesDefaultIdentity(trajectoryConstraintFor(loopRoot, loopSid)))) return {
14554
12236
  boundary: -1,
14555
12237
  promoted: true
14556
12238
  };
@@ -14669,7 +12351,7 @@ function registerBootstrap(ctx) {
14669
12351
  const depth = session?.header?.delegationDepth ?? 0;
14670
12352
  const sid = typeof session?.id === "string" ? session.id : void 0;
14671
12353
  if (sid === void 0) return;
14672
- if (sid.startsWith("cheap-worker-") || isTrajectoryConstrained(root, sid)) return;
12354
+ if (sid.startsWith("cheap-worker-") || replacesDefaultIdentity(trajectoryConstraintFor(root, sid))) return;
14673
12355
  if (depth === 0) {
14674
12356
  if (hasUserMessageHistory(session)) return;
14675
12357
  } else if (anchoredSessions.has(sid)) return;
@@ -15209,12 +12891,209 @@ function registerKeeper(ctx, opts = {}) {
15209
12891
  });
15210
12892
  }
15211
12893
  //#endregion
12894
+ //#region src/trajectory/model-runtime.ts
12895
+ /**
12896
+ * trajectory/model-runtime.ts — **`trajectory.json` 的 `model` 字段:每轮求值 + 运行时热切换**
12897
+ *
12898
+ * ## 这个模块存在的唯一理由(owner 2026-10-20 令,逐字)
12899
+ *
12900
+ * > 「我认为认知轨迹作为统一概念 **设定不应根据入口不同而有变化 所以应当直接生效 运行时热加载**」
12901
+ *
12902
+ * ## 旧实现错在哪(D182 的事实底数)
12903
+ *
12904
+ * `model` 的**唯一消费点**曾是 `createTrajectoryAgent` 建 agent 时,而它的**唯一调用方**是
12905
+ * IM 入站路由 ⇒ 对**已经存在的会话**(web 会话、`send-now` 冷启的载体……)**完全不生效**。
12906
+ * 而 `container_admin config list` 把它印成 `'(cheap-worker default)'`,看上去像个统一设定。
12907
+ * ⇒ **同一个字段在 A 入口生效、在 B 入口不生效** —— 正是 owner 要禁的形态。
12908
+ *
12909
+ * ## 修法:接宿主**自带**的原语,不复刻语义
12910
+ *
12911
+ * `@deepseek-ai/dsh-agent` 导出 `installModelSelection(agentCtx, selection)`,它挂三条缝:
12912
+ *
12913
+ * | 缝 | 作用 |
12914
+ * |---|---|
12915
+ * | `system-prompt/assemble` | 把选中路由写进提示词变量快照(保证"这一步用哪个模型"一致) |
12916
+ * | **`agent/request`** | **把冻结的调用配置替换成 `selection.current`** ⇒ 这就是"下一轮生效" |
12917
+ * | `agent/pre-step` | 路由变化时追加 `[model changed: …]` 持久提示 |
12918
+ *
12919
+ * 🔴 **为什么必须直接调它而不是自己挂 `agent/request`**(本仓既有纪律):
12920
+ * 自己复刻就要重做第三条的三态判断(reject/空步/aborted)与 `requestHeader` 比对,
12921
+ * **复刻错了会得到与事实相反的结论**。宿主已实现 ⇒ 直接调用。
12922
+ *
12923
+ * 🔴 **跨宿主档位**:`_tmp/host-*` 全部 **10 个**快照(`0.1.5-rc.1` … `0.2.1-rc.2`)
12924
+ * 逐个核对,该导出**全部存在且签名一致**(唯一差异在宿主内部的提示 `source` 形状,
12925
+ * **不由我们构造**)⇒ 无需宿主升级、无需分档分支。
12926
+ *
12927
+ * ## 三条硬边界(少一条就会重演 S196 的坑)
12928
+ *
12929
+ * 1. **未写 `model` ⇒ 不装这条缝**(`undefined` 原样返回)。⛔ **绝不回退 `cheap-worker.defaultModel`**
12930
+ * —— 那个回退把"我想声明自述"变成"我被降级到杂工模型",而用户完全不会预期到。
12931
+ * 2. **切换会留下宿主的 `[model changed: …]` 提示**。这是**特性不是噪声**:
12932
+ * 否则模型不知道自己换了,它会照旧解释上方那些由**另一个模型**生成的回合。
12933
+ * 3. **模型名非法 ⇒ 不切 + 响亮告警**,绝不静默停在旧模型上("改了没生效"是最难查的一类)。
12934
+ *
12935
+ * ## 归属(D23)
12936
+ * **机制在 ACC**(怎么读、怎么装、何时刷新、非法怎么办);**内容在 CCC**(`trajectory.json` 写哪个模型)。
12937
+ */
12938
+ /**
12939
+ * 从轨迹声明里取模型选择(**每轮调用**,故不缓存)。
12940
+ *
12941
+ * @param root CCC 根
12942
+ * @param dirName 轨迹目录名
12943
+ * @returns `{ provider?, model }`;**未写 `model` ⇒ `undefined`(= 不动当前模型)**
12944
+ *
12945
+ * ⚠️ 读盘失败/坏 JSON ⇒ `undefined` 并**不抛**:一次读失败不该让整轮失败
12946
+ * (`readTrajectoryConstraintAt` 内部已对坏 JSON 告警)。
12947
+ */
12948
+ function readModelSelection(root, dirName) {
12949
+ let profile;
12950
+ try {
12951
+ profile = readTrajectoryConstraintAt(join(sessionsRoot(root), dirName), root);
12952
+ } catch {
12953
+ return;
12954
+ }
12955
+ const raw = profile?.model?.trim();
12956
+ if (raw === void 0 || raw === "") return void 0;
12957
+ const { provider, model } = splitModel(raw);
12958
+ if (model.trim() === "") return void 0;
12959
+ return provider === void 0 ? { model } : {
12960
+ provider,
12961
+ model
12962
+ };
12963
+ }
12964
+ /** 已装的模型选择(进程级;`sessionId → selection`,与 tool-restrictions 同构) */
12965
+ const installed = /* @__PURE__ */ new Map();
12966
+ /** 会话销毁时清理 */
12967
+ function forgetModelRuntime(sessionId) {
12968
+ const cur = installed.get(sessionId);
12969
+ if (cur === void 0) return;
12970
+ try {
12971
+ cur.dispose();
12972
+ } catch {}
12973
+ installed.delete(sessionId);
12974
+ }
12975
+ /**
12976
+ * 给这个 agent 装**模型运行时热切换**(每轮重读 `trajectory.json`)。
12977
+ *
12978
+ * 🔴 **只在轨迹声明了 `model` 时装**(D182 边界 ①):
12979
+ * 装了缝而 `current` 为 `undefined` 会**覆盖**宿主已有的模型选择 ⇒ 等于把会话的模型清空。
12980
+ * 故"没写" **必须** 表现为"这条缝不存在",而不是"缝在、值为空"。
12981
+ *
12982
+ * 🔴 **每轮刷新**:`agent/pre-step` 每步都调它,求值器**重读磁盘**
12983
+ * (`readTrajectoryConstraintAt` 无缓存)⇒ 改 `trajectory.json` 下一轮即生效,
12984
+ * **与"谁创建了这个会话"无关**(这就是 owner 的令)。
12985
+ *
12986
+ * @param agent 目标 agent
12987
+ * @param root CCC 根
12988
+ * @param dirName 该 agent 绑定的轨迹目录名
12989
+ * @returns 是否装了缝(`false` = 该轨迹没声明 model,未改动任何东西)
12990
+ */
12991
+ function syncTrajectoryModel(agent, root, dirName) {
12992
+ const sessionId = agent.session?.id ?? "global";
12993
+ const wanted = readModelSelection(root, dirName);
12994
+ if (wanted === void 0) {
12995
+ forgetModelRuntime(sessionId);
12996
+ return false;
12997
+ }
12998
+ const existing = installed.get(sessionId);
12999
+ if (existing !== void 0) {
13000
+ existing.current = wanted;
13001
+ return true;
13002
+ }
13003
+ const install = resolveInstallModelSelection();
13004
+ if (install === null) {
13005
+ console.warn(`[serenity-hooks] ✗ 轨迹 "${dirName}" 声明了 model,但**宿主未导出 installModelSelection** ⇒ 模型热切换无法生效(当前会话模型保持不变)。这是宿主契约变化,请核对 @deepseek-ai/dsh-agent 的导出。`);
13006
+ return false;
13007
+ }
13008
+ const selection = {
13009
+ current: wanted,
13010
+ assembled: void 0
13011
+ };
13012
+ try {
13013
+ const dispose = install(agent.ctx, selection);
13014
+ installed.set(sessionId, {
13015
+ ...selection,
13016
+ dispose
13017
+ });
13018
+ return true;
13019
+ } catch (err) {
13020
+ console.warn(`[serenity-hooks] ✗ 轨迹 "${dirName}" 的模型热切换装配失败(会话模型保持不变): ${String(err?.message ?? err)}`);
13021
+ return false;
13022
+ }
13023
+ }
13024
+ /**
13025
+ * 取宿主的 `installModelSelection`。
13026
+ *
13027
+ * 🔴 **为什么是 `createRequire` 动态取,而不是顶层 `import`**(R↓,两个理由):
13028
+ * ① 本仓对 `@deepseek-ai/dsh-agent` 的**全部**引用此前都是 `import type`(零运行时依赖);
13029
+ * 改成顶层运行时 import 会把它变成**硬依赖**——而它是 `peerDependencies`,
13030
+ * 宿主没装好时**整个插件起不来**,那比"模型不切换"严重得多。
13031
+ * ② 这是一个**可选能力**:拿不到就告警并保持会话原模型(`null` 分支),插件其余部分照常工作。
13032
+ *
13033
+ * ⚠️ 本仓是 ESM(`package.json` `"type": "module"`)⇒ **裸 `require` 不可用**,
13034
+ * 必须走 `createRequire(import.meta.url)`(同 `diagram/rasterize.ts` 的既有做法)。
13035
+ *
13036
+ * ⚠️ 解析结果**缓存**(成功或确实没有都缓存):避免每轮重复 require 与重复告警刷屏。
13037
+ */
13038
+ let resolveCache;
13039
+ function resolveInstallModelSelection() {
13040
+ if (resolveCache !== void 0) return resolveCache;
13041
+ try {
13042
+ const mod = createRequire(import.meta.url)("@deepseek-ai/dsh-agent");
13043
+ resolveCache = typeof mod.installModelSelection === "function" ? mod.installModelSelection : null;
13044
+ } catch {
13045
+ resolveCache = null;
13046
+ }
13047
+ return resolveCache;
13048
+ }
13049
+ //#endregion
15212
13050
  //#region src/seams/context.ts
13051
+ /**
13052
+ * 该会话的轨迹**是否自带章程**(= 该不该旁路 ACC 身份类默认行为)。D183。
13053
+ *
13054
+ * 🔴 判据唯一入口 = `replacesDefaultIdentity`(见 `policy/profile.ts` 的判据表)。
13055
+ * ⛔ 不要在这里写 `isTrajectoryConstrained` —— 那个只回答"有没有声明",
13056
+ * 用它做旁路就是 owner 2026-10-20 指出的那个 bug(只写 resident 也丢身份)。
13057
+ */
13058
+ function hasTrajectoryOwnCharter(root, sessionId) {
13059
+ if (typeof sessionId !== "string" || sessionId === "") return false;
13060
+ try {
13061
+ return replacesDefaultIdentity(trajectoryConstraintFor(root, sessionId));
13062
+ } catch {
13063
+ return false;
13064
+ }
13065
+ }
13066
+ /**
13067
+ * 该 agent 绑定的轨迹目录名(没有 ⇒ `null`)。
13068
+ *
13069
+ * 🔴 判据唯一入口 = `trajectoryConstraintFor`(它内部并取"持久绑定 + 内存注册表"两来源,
13070
+ * 见 `policy/profile.ts` 的 R↓)。⛔ 不要在这里另写一套解析 —— 两份判据会漂。
13071
+ */
13072
+ function trajectoryDirNameOf(agent, root) {
13073
+ const sessionId = agent.session?.id;
13074
+ if (typeof sessionId !== "string" || sessionId === "") return null;
13075
+ try {
13076
+ return trajectoryConstraintFor(root, sessionId)?.dirName ?? null;
13077
+ } catch {
13078
+ return null;
13079
+ }
13080
+ }
13081
+ /**
13082
+ * 同步该 agent 的**模型热切换**(D182)。
13083
+ *
13084
+ * 🔴 把它做成"按 agent 解析目录名 ⇒ 交给 `trajectory/model-runtime`"的薄适配,
13085
+ * 是为了让 `model-runtime` 保持**零 seams 依赖**(可独立单测)。
13086
+ */
13087
+ function syncTrajectoryModelForSession(agent, root) {
13088
+ const dirName = trajectoryDirNameOf(agent, root);
13089
+ if (dirName === null) return;
13090
+ syncTrajectoryModel(agent, root, dirName);
13091
+ }
15213
13092
  const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
15214
13093
  /** 从 dsh 会话日志读标题(latest-wins `session/title` 事件;无返回 null)——供标题 reconcile(U3) */
15215
13094
  function readDshSessionTitle(session) {
15216
13095
  try {
15217
- const events = sessionEvents$1(session);
13096
+ const events = sessionEvents(session);
15218
13097
  for (let i = events.length - 1; i >= 0; i--) {
15219
13098
  const e = events[i];
15220
13099
  if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
@@ -15279,7 +13158,7 @@ function shouldAutoRestore(agent, root) {
15279
13158
  if (session.header?.parentSession) return false;
15280
13159
  if (session.id?.startsWith("cheap-worker-")) return false;
15281
13160
  const cccRoot = root !== void 0 ? root : cccRootForCwd(session.header?.cwd ?? process.cwd());
15282
- if (cccRoot && isTrajectoryConstrained(cccRoot, session.id)) return false;
13161
+ if (cccRoot && hasTrajectoryOwnCharter(cccRoot, session.id)) return false;
15283
13162
  return true;
15284
13163
  }
15285
13164
  /**
@@ -15289,7 +13168,7 @@ function shouldAutoRestore(agent, root) {
15289
13168
  */
15290
13169
  function shouldRestoreActive(agent, root) {
15291
13170
  if (!shouldAutoRestore(agent, root)) return false;
15292
- return sessionEvents$1(agent.session).length > 0;
13171
+ return sessionEvents(agent.session).length > 0;
15293
13172
  }
15294
13173
  function registerContext(ctx, opts = {}) {
15295
13174
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
@@ -15302,7 +13181,7 @@ function registerContext(ctx, opts = {}) {
15302
13181
  } catch {}
15303
13182
  if (!root) return;
15304
13183
  const key = agentKey(agent);
15305
- if (isTrajectoryConstrained(root, key)) return;
13184
+ if (hasTrajectoryOwnCharter(root, key)) return;
15306
13185
  registerEntrySkillSection(agent, root);
15307
13186
  registerCognitiveTrajectorySkillSection(agent, root);
15308
13187
  const scope = agentScope(agent);
@@ -15320,7 +13199,7 @@ function registerContext(ctx, opts = {}) {
15320
13199
  };
15321
13200
  }
15322
13201
  if (!restored) {
15323
- const info = parseSessionContextFromEvents(sessionEvents$1(dshSession));
13202
+ const info = parseSessionContextFromEvents(sessionEvents(dshSession));
15324
13203
  if (info) {
15325
13204
  const abs = info.mdPath.startsWith(root) ? info.mdPath : resolve(root, info.mdPath);
15326
13205
  if (existsSync(abs)) restored = {
@@ -15373,6 +13252,9 @@ function registerContext(ctx, opts = {}) {
15373
13252
  try {
15374
13253
  syncHostScheduleToolHiding(agent, root);
15375
13254
  } catch {}
13255
+ try {
13256
+ syncTrajectoryModelForSession(agent, root);
13257
+ } catch {}
15376
13258
  };
15377
13259
  if (opts.seedOnStart ?? true) ctx.on("agent/created", async (payload) => {
15378
13260
  try {
@@ -15385,10 +13267,15 @@ function registerContext(ctx, opts = {}) {
15385
13267
  const root = cccRootForCwd(cwd);
15386
13268
  const key = agentKey(agent);
15387
13269
  const downstream = await next();
15388
- if (root) try {
15389
- syncTrajectoryConstraintToolVisibility(agent, root);
15390
- } catch {}
15391
- if (root && isTrajectoryConstrained(root, key)) return downstream;
13270
+ if (root) {
13271
+ try {
13272
+ syncTrajectoryConstraintToolVisibility(agent, root);
13273
+ } catch {}
13274
+ try {
13275
+ syncTrajectoryModelForSession(agent, root);
13276
+ } catch {}
13277
+ }
13278
+ if (root && hasTrajectoryOwnCharter(root, key)) return downstream;
15392
13279
  try {
15393
13280
  syncCccToolVisibility(agent, root);
15394
13281
  } catch {}
@@ -15437,7 +13324,10 @@ function registerCompactRetention(ctx, opts = {}) {
15437
13324
  const cwd = agent.session?.header?.cwd ?? process.cwd();
15438
13325
  const root = cccRootForCwd(cwd);
15439
13326
  if (!root) return;
15440
- if (isTrajectoryConstrained(root, String(session.id ?? ""))) return;
13327
+ const sid = String(session.id ?? "");
13328
+ try {
13329
+ if (replacesDefaultIdentity(trajectoryConstraintFor(root, sid))) return;
13330
+ } catch {}
15441
13331
  try {
15442
13332
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
15443
13333
  } catch {}
@@ -15932,7 +13822,8 @@ function registerStatusApi(ctx, opts = {}) {
15932
13822
  const settings = readAdvancedSettings();
15933
13823
  let known = [];
15934
13824
  try {
15935
- known = projectKnownWorkspaces(hostService(ctx, "workspaceRegistry")?.list?.() ?? [], settings.gateway.workspaces);
13825
+ const registry = hostService(ctx, "workspaceRegistry");
13826
+ known = projectKnownWorkspaces(registry?.list?.() ?? [], settings.gateway.workspaces);
15936
13827
  } catch {}
15937
13828
  sendJson$2(res, 200, {
15938
13829
  config: toWire(settings),
@@ -15942,7 +13833,8 @@ function registerStatusApi(ctx, opts = {}) {
15942
13833
  }
15943
13834
  if (req.method === "PUT") {
15944
13835
  const raw = await readBody$2(req, 131072);
15945
- const saved = applyWirePatch(JSON.parse(raw).config ?? {});
13836
+ const body = JSON.parse(raw);
13837
+ const saved = applyWirePatch(body.config ?? {});
15946
13838
  try {
15947
13839
  ctx.emit?.("serenity/config-updated");
15948
13840
  } catch {}
@@ -15992,11 +13884,11 @@ function registerStatusApi(ctx, opts = {}) {
15992
13884
  sendJson$2(res, 400, { error: chk.error });
15993
13885
  return;
15994
13886
  }
15995
- const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
13887
+ const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
15996
13888
  const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
15997
13889
  const settings = readWeixinSettings(chk.root);
15998
13890
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
15999
- const { imCredentialBound } = await import("./route-COOFLTpU.js").then((n) => n.c);
13891
+ const { imCredentialBound } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
16000
13892
  const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
16001
13893
  const accounts = (settings.accounts ?? []).map((a) => {
16002
13894
  const channel = normalizeAccountChannel(a.channel);
@@ -16033,7 +13925,7 @@ function registerStatusApi(ctx, opts = {}) {
16033
13925
  const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
16034
13926
  const { randomUUID } = await import("node:crypto");
16035
13927
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
16036
- const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
13928
+ const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
16037
13929
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
16038
13930
  const loginKey = randomUUID();
16039
13931
  weixinLogins.set(loginKey, {
@@ -16054,7 +13946,7 @@ function registerStatusApi(ctx, opts = {}) {
16054
13946
  sendJson$2(res, 400, { error: "missing accountId" });
16055
13947
  return;
16056
13948
  }
16057
- const { removeWeixinAccount } = await import("./route-COOFLTpU.js").then((n) => n.c);
13949
+ const { removeWeixinAccount } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
16058
13950
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
16059
13951
  removeWeixinAccount(root, body.accountId);
16060
13952
  syncCccBridge(ctx, root);
@@ -16080,8 +13972,8 @@ function registerStatusApi(ctx, opts = {}) {
16080
13972
  trajectory: code
16081
13973
  });
16082
13974
  }
16083
- const { saveWeixinRoutes } = await import("./route-COOFLTpU.js").then((n) => n.c);
16084
- const { findDirNameByCode } = await import("./profile-DplLGGqo.js").then((n) => n.i);
13975
+ const { saveWeixinRoutes } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
13976
+ const { findDirNameByCode } = await import("./profile-C7BuyZK7.js").then((n) => n.i);
16085
13977
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
16086
13978
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
16087
13979
  return;
@@ -16092,7 +13984,7 @@ function registerStatusApi(ctx, opts = {}) {
16092
13984
  }
16093
13985
  if (body.action === "set-enabled") {
16094
13986
  const enabled = body.enabled === true;
16095
- const { readWeixinSettings, setWeixinEnabled } = await import("./route-COOFLTpU.js").then((n) => n.c);
13987
+ const { readWeixinSettings, setWeixinEnabled } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
16096
13988
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
16097
13989
  const settings = readWeixinSettings(root);
16098
13990
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -16115,7 +14007,7 @@ function registerStatusApi(ctx, opts = {}) {
16115
14007
  sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
16116
14008
  return;
16117
14009
  }
16118
- const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-COOFLTpU.js").then((n) => n.c);
14010
+ const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
16119
14011
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
16120
14012
  const before = readWeixinSettings(root);
16121
14013
  const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
@@ -16165,7 +14057,7 @@ function registerStatusApi(ctx, opts = {}) {
16165
14057
  return;
16166
14058
  }
16167
14059
  const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
16168
- const { readWeixinSettings } = await import("./route-COOFLTpU.js").then((n) => n.c);
14060
+ const { readWeixinSettings } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
16169
14061
  const settings = readWeixinSettings(login.root);
16170
14062
  const status = await pollQRStatus({
16171
14063
  baseUrl: void 0,
@@ -16180,7 +14072,7 @@ function registerStatusApi(ctx, opts = {}) {
16180
14072
  });
16181
14073
  return;
16182
14074
  }
16183
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-COOFLTpU.js").then((n) => n.c);
14075
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-Dr0SFD8A.js").then((n) => n.c);
16184
14076
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
16185
14077
  const accountId = nextWeixinAccountId(settings);
16186
14078
  upsertWeixinAccount(login.root, {
@@ -18193,7 +16085,7 @@ function parseAnchorMdPath(session) {
18193
16085
  try {
18194
16086
  const nodes = [...session.surface.nodes];
18195
16087
  if (nodes.length === 0) return null;
18196
- const events = sessionEvents$1(session);
16088
+ const events = sessionEvents(session);
18197
16089
  if (events.length === 0) return null;
18198
16090
  const event = events[nodes[0]];
18199
16091
  if (!event) return null;
@@ -18229,7 +16121,7 @@ function sessionNameFromMdPath(mdPath) {
18229
16121
  function resolveSessionMdPath(root, scope, session) {
18230
16122
  const candidates = [];
18231
16123
  candidates.push(getActiveSessionInfo(scope)?.mdPath ?? null);
18232
- const events = sessionEvents$1(session);
16124
+ const events = sessionEvents(session);
18233
16125
  if (events.length > 0) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
18234
16126
  candidates.push(readLastBound(session)?.mdPath ?? null);
18235
16127
  candidates.push(parseAnchorMdPath(session));
@@ -18342,7 +16234,7 @@ function performRebuild(session, pending, meter) {
18342
16234
  if (targets.length === 0) return false;
18343
16235
  if (meter) {
18344
16236
  let shadowedTokenCount = 0;
18345
- const events = sessionEvents$1(session);
16237
+ const events = sessionEvents(session);
18346
16238
  for (const seq of targets) {
18347
16239
  const event = events[seq];
18348
16240
  if (!event) continue;
@@ -20388,7 +18280,7 @@ function isExternalFaceSession(sessionId, root) {
20388
18280
  * 否则 think 内提及内部机制词(思考过程必然推演机制)会误打回。复用 stripThink
20389
18281
  * (v1.26.8 状态机,弃正则——同 v1.27.1 微信桥回复链路)。 */
20390
18282
  function lastAssistantText(agent) {
20391
- const events = sessionEvents$1(agent.session);
18283
+ const events = sessionEvents(agent.session);
20392
18284
  for (let i = events.length - 1; i >= 0; i--) {
20393
18285
  const e = events[i];
20394
18286
  if (e && e.type === "assistant/message") {
@@ -21063,7 +18955,7 @@ const MEDIA_MAX_BYTES = 20971520;
21063
18955
  */
21064
18956
  async function resolveCccRootByAlias(ctx, alias) {
21065
18957
  try {
21066
- const { listCccs } = await import("./ccc-roots-rwgzt_0F.js").then((n) => n.a);
18958
+ const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
21067
18959
  const entries = await listCccs(ctx);
21068
18960
  const byName = entries.find((e) => e.name === alias);
21069
18961
  if (byName) return byName.root;
@@ -21131,7 +19023,7 @@ async function resolveInboundTarget(ctx, input) {
21131
19023
  });
21132
19024
  return null;
21133
19025
  }
21134
- const { findDirNameByCode } = await import("./profile-DplLGGqo.js").then((n) => n.i);
19026
+ const { findDirNameByCode } = await import("./profile-C7BuyZK7.js").then((n) => n.i);
21135
19027
  if (findDirNameByCode(root, target.role) === null) {
21136
19028
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
21137
19029
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -21943,7 +19835,7 @@ function syncHumanChannel(ctx) {
21943
19835
  (async () => {
21944
19836
  try {
21945
19837
  stopMachineLoops();
21946
- const { listCccs } = await import("./ccc-roots-rwgzt_0F.js").then((n) => n.a);
19838
+ const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
21947
19839
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
21948
19840
  } catch {}
21949
19841
  })();
@@ -22207,7 +20099,7 @@ function matchCcc(input, candidates) {
22207
20099
  }
22208
20100
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
22209
20101
  async function collectCandidates(ctx) {
22210
- const { listCccs } = await import("./ccc-roots-rwgzt_0F.js").then((n) => n.a);
20102
+ const { listCccs } = await import("./ccc-roots-D3ty1C8P.js").then((n) => n.a);
22211
20103
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
22212
20104
  const seen = /* @__PURE__ */ new Set();
22213
20105
  const out = [];
@@ -23658,7 +21550,7 @@ function createImBridgeTool() {
23658
21550
  */
23659
21551
  function readSessionTitle(session) {
23660
21552
  try {
23661
- const events = sessionEvents$1(session);
21553
+ const events = sessionEvents(session);
23662
21554
  for (let i = events.length - 1; i >= 0; i--) {
23663
21555
  const e = events[i];
23664
21556
  if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();