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