@shgroup/dsh-serenity-hooks 1.26.13 → 1.26.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dsh.plugin.json +1 -1
- package/experiments/autotrajectory/scripts/autotrajectory-exp.ts +123 -1
- package/lib/client.js +117 -1
- package/lib/index.js +716 -505
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { a as findSerenityRoot, c as matchBlacklist, d as readHandymanConfig, f as readUtf8, i as findGitRoot, l as pathInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as resolveInside, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CfDrlfA7.js";
|
|
1
2
|
import z from "@deepseek-ai/schemastery";
|
|
2
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
4
|
import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
@@ -23,144 +24,6 @@ var __exportAll = (all, no_symbols) => {
|
|
|
23
24
|
return target;
|
|
24
25
|
};
|
|
25
26
|
//#endregion
|
|
26
|
-
//#region src/ccc.ts
|
|
27
|
-
/**
|
|
28
|
-
* ccc.ts — CCC 纯逻辑层(零 DSH 依赖,可独立单测)
|
|
29
|
-
*
|
|
30
|
-
* 职责:CCC 根检测(P1)、git 检测(P2)、serenity.json 配置读取(.opencode 规范位置,.dsh 回退)、
|
|
31
|
-
* 路径守卫(P3 语义)、安全模式黑名单匹配。
|
|
32
|
-
*
|
|
33
|
-
* 由 tools/ 与 seams/ 复用;逻辑移植自 dsh-serenity-plugin v0.1-v0.2
|
|
34
|
-
* runner(本项目自有代码,非 opencode-serenity-plugin 源码)。
|
|
35
|
-
*/
|
|
36
|
-
function findSerenityRoot(cwd) {
|
|
37
|
-
let current = resolve(cwd);
|
|
38
|
-
while (true) {
|
|
39
|
-
const marker = resolve(current, ".serenity");
|
|
40
|
-
if (existsSync(marker) && statSync(marker).isFile()) return current;
|
|
41
|
-
const parent = dirname(current);
|
|
42
|
-
if (parent === current) return null;
|
|
43
|
-
current = parent;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function findGitRoot(cwd) {
|
|
47
|
-
let current = resolve(cwd);
|
|
48
|
-
while (true) {
|
|
49
|
-
if (existsSync(resolve(current, ".git"))) return current;
|
|
50
|
-
const parent = dirname(current);
|
|
51
|
-
if (parent === current) return null;
|
|
52
|
-
current = parent;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* 前缀判定:abs 是否位于 rootAbs 之内。
|
|
57
|
-
* caseInsensitive(Windows 盘符/路径大小写不敏感)由调用方按平台传入;
|
|
58
|
-
* 边界必须是路径分隔符(`\` 或 `/` 任一)——不依赖平台 sep,跨平台语义一致:
|
|
59
|
-
* - 跨盘符绝对路径(root 在 D:\、target 在 C:\)前缀不匹配 → outside
|
|
60
|
-
* - 兄弟目录前缀陷阱(home vs home2)边界非分隔符 → outside
|
|
61
|
-
* (旧实现用 path.relative().startsWith('..')——跨盘时 relative 返回绝对路径原文,
|
|
62
|
-
* 不以 `..` 开头 → 漏判放行,见 Windows 兼容审计问题 1。)
|
|
63
|
-
*/
|
|
64
|
-
function pathInside(rootAbs, abs, caseInsensitive = process.platform === "win32") {
|
|
65
|
-
const r = caseInsensitive ? rootAbs.toLowerCase() : rootAbs;
|
|
66
|
-
const a = caseInsensitive ? abs.toLowerCase() : abs;
|
|
67
|
-
if (a === r) return true;
|
|
68
|
-
if (!a.startsWith(r)) return false;
|
|
69
|
-
const next = a[r.length];
|
|
70
|
-
return next === "\\" || next === "/";
|
|
71
|
-
}
|
|
72
|
-
function classifyPath(p, root) {
|
|
73
|
-
const rootAbs = resolve(root);
|
|
74
|
-
const abs = resolve(p);
|
|
75
|
-
const ci = process.platform === "win32";
|
|
76
|
-
if (ci ? abs.toLowerCase() === rootAbs.toLowerCase() : abs === rootAbs) return "same";
|
|
77
|
-
return pathInside(rootAbs, abs, ci) ? "inside" : "outside";
|
|
78
|
-
}
|
|
79
|
-
function resolveInside(root, p) {
|
|
80
|
-
const abs = resolve(root, p);
|
|
81
|
-
if (classifyPath(abs, root) === "outside") throw new Error(`Path escape blocked: "${p}" resolves outside "${root}"`);
|
|
82
|
-
return abs;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* 解析 handyman 配置(v1.24.0:取代 loop.defaultModel;不兼容旧 loop 配置——用户拍板)。
|
|
86
|
-
* @returns 白名单 + 缺省模型 + 上限;models 未配置返回 null(工具应报错要求配置)
|
|
87
|
-
*/
|
|
88
|
-
function readHandymanConfig(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
89
|
-
const cfg = loadSerenityConfig(root, paths);
|
|
90
|
-
const models = cfg.handyman?.models;
|
|
91
|
-
if (!Array.isArray(models) || models.length === 0) return null;
|
|
92
|
-
const clean = models.map((m) => m.trim()).filter(Boolean);
|
|
93
|
-
if (clean.length === 0) return null;
|
|
94
|
-
const defaultModel = cfg.handyman?.defaultModel?.trim();
|
|
95
|
-
return {
|
|
96
|
-
models: clean,
|
|
97
|
-
defaultModel: defaultModel !== void 0 && clean.includes(defaultModel) ? defaultModel : clean[0],
|
|
98
|
-
maxRounds: cfg.handyman?.maxRounds ?? 100,
|
|
99
|
-
maxParallel: cfg.handyman?.maxParallel ?? 10
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* ACC 依赖的 CCC 配置路径(S134 修正:兼容历史——历史在 .opencode,规范位置 = .opencode;
|
|
104
|
-
* .dsh 仅作 dsh 运行时回退,不优先)。
|
|
105
|
-
*/
|
|
106
|
-
const DEFAULT_SERENITY_CONFIG_PATHS = [".opencode/serenity.json", ".dsh/serenity.json"];
|
|
107
|
-
/**
|
|
108
|
-
* 读取 UTF-8 文件并剥离 BOM(Windows 审计问题 16):PowerShell/Windows 编辑器
|
|
109
|
-
* 写出的 BOM(\uFEFF)会让 JSON.parse 抛错(配置静默变空)或 frontmatter 检测失败(技能被丢弃)。
|
|
110
|
-
*/
|
|
111
|
-
function readUtf8(path) {
|
|
112
|
-
return readFileSync(path, "utf-8").replace(/^\uFEFF/, "");
|
|
113
|
-
}
|
|
114
|
-
function loadSerenityConfig(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
115
|
-
for (const candidate of paths) {
|
|
116
|
-
const p = resolve(root, candidate);
|
|
117
|
-
if (!existsSync(p)) continue;
|
|
118
|
-
try {
|
|
119
|
-
return JSON.parse(readUtf8(p));
|
|
120
|
-
} catch {
|
|
121
|
-
return {};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return {};
|
|
125
|
-
}
|
|
126
|
-
const SAFE_MODE_MARKER = ".serenity-safe-on";
|
|
127
|
-
function isSafeModeOn(root) {
|
|
128
|
-
return existsSync(resolve(root, SAFE_MODE_MARKER));
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* 读取黑名单(对齐 osp readBlacklist):支持两种条目格式——
|
|
132
|
-
* string:".secrets/" 或 "regex:^..."(前缀 / 正则)
|
|
133
|
-
* object:{ "pattern": ".secrets/", "message": "自定义提示" }
|
|
134
|
-
* dsp v1.17.4 前只支持 string(对象会被 String() 成 "[object Object]" → 规则失效,不拦截)。
|
|
135
|
-
*/
|
|
136
|
-
function readBlacklist(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
137
|
-
const rules = loadSerenityConfig(root, paths).safeMode?.blacklist;
|
|
138
|
-
if (!Array.isArray(rules)) return [];
|
|
139
|
-
const out = [];
|
|
140
|
-
for (const item of rules) if (typeof item === "string") {
|
|
141
|
-
if (item) out.push({ pattern: item });
|
|
142
|
-
} else if (item && typeof item === "object" && typeof item.pattern === "string") {
|
|
143
|
-
const obj = item;
|
|
144
|
-
const p = obj.pattern;
|
|
145
|
-
if (p) out.push({
|
|
146
|
-
pattern: p,
|
|
147
|
-
message: typeof obj.message === "string" ? obj.message : void 0
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
152
|
-
/** 匹配黑名单规则;命中返回条目,未命中返回 null。前缀匹配 / regex: 前缀(对齐 osp)。Windows:规则反斜杠归一化为正斜杠与 rel 对齐 */
|
|
153
|
-
function matchBlacklist(relPath, rules) {
|
|
154
|
-
for (const rule of rules) if (rule.pattern.startsWith("regex:")) try {
|
|
155
|
-
if (new RegExp(rule.pattern.slice(6)).test(relPath)) return rule;
|
|
156
|
-
} catch {}
|
|
157
|
-
else {
|
|
158
|
-
const pat = rule.pattern.split("\\").join("/");
|
|
159
|
-
if (relPath.startsWith(pat)) return rule;
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
//#endregion
|
|
164
27
|
//#region src/fs-ops.ts
|
|
165
28
|
/**
|
|
166
29
|
* fs-ops.ts — cc_fs 纯操作层(零 DSH 依赖,可独立单测)
|
|
@@ -947,7 +810,8 @@ function extractAction(exec) {
|
|
|
947
810
|
function registerGuards(ctx, opts = {}) {
|
|
948
811
|
const configPaths = opts.configPaths;
|
|
949
812
|
const evaluate = (exec) => {
|
|
950
|
-
const
|
|
813
|
+
const cwd = resolveAgentCwd(exec);
|
|
814
|
+
const root = findSerenityRoot(cwd);
|
|
951
815
|
if (!root) return { kind: "allow" };
|
|
952
816
|
const safeModeOn = isSafeModeOn(root);
|
|
953
817
|
const blacklist = readBlacklist(root, configPaths);
|
|
@@ -7085,150 +6949,651 @@ const skiffAdminTool = defineTool({
|
|
|
7085
6949
|
}
|
|
7086
6950
|
});
|
|
7087
6951
|
//#endregion
|
|
7088
|
-
//#region src/
|
|
6952
|
+
//#region src/autotrajectory.ts
|
|
6953
|
+
var autotrajectory_exports = /* @__PURE__ */ __exportAll({
|
|
6954
|
+
AUTO_DIR_SUFFIX: () => AUTO_DIR_SUFFIX,
|
|
6955
|
+
DEFAULT_AVOID_HOURS: () => DEFAULT_AVOID_HOURS,
|
|
6956
|
+
DEFAULT_BIAS_PROVIDER: () => DEFAULT_BIAS_PROVIDER,
|
|
6957
|
+
MOTIVATION_MARKER: () => MOTIVATION_MARKER,
|
|
6958
|
+
TICK_MS: () => TICK_MS,
|
|
6959
|
+
beijingHour: () => beijingHour,
|
|
6960
|
+
buildWakeMessage: () => buildWakeMessage,
|
|
6961
|
+
diagLive: () => diagLive,
|
|
6962
|
+
fetchBiasContent: () => fetchBiasContent,
|
|
6963
|
+
getAutoTrajectoryStatus: () => getAutoTrajectoryStatus,
|
|
6964
|
+
inAllowedWakeWindow: () => inAllowedWakeWindow,
|
|
6965
|
+
isAutoTrajectorySession: () => isAutoTrajectorySession,
|
|
6966
|
+
listLiveSessions: () => listLiveSessions,
|
|
6967
|
+
performAutoTrajectoryWake: () => performAutoTrajectoryWake,
|
|
6968
|
+
readAutoTrajectorySettings: () => readAutoTrajectorySettings,
|
|
6969
|
+
readSelfGeneratedMotivation: () => readSelfGeneratedMotivation,
|
|
6970
|
+
registerAutoTrajectory: () => registerAutoTrajectory,
|
|
6971
|
+
resolveAutoTrajectoryCcc: () => resolveAutoTrajectoryCcc,
|
|
6972
|
+
resolveTargetMd: () => resolveTargetMd,
|
|
6973
|
+
shouldWake: () => shouldWake
|
|
6974
|
+
});
|
|
6975
|
+
const PLUGIN_SOURCE$3 = {
|
|
6976
|
+
kind: "plugin",
|
|
6977
|
+
plugin: "dsh-serenity-hooks"
|
|
6978
|
+
};
|
|
6979
|
+
/** 偏见内容提供者脚本缺省名(CCC 根目录下;用户拍板:tool 直接运行,缺失报错要求实现) */
|
|
6980
|
+
const DEFAULT_BIAS_PROVIDER = "autotrajectory-bias.ts";
|
|
6981
|
+
/** 自主轨迹会话目录后缀标志(用户拍板:后缀) */
|
|
6982
|
+
const AUTO_DIR_SUFFIX = "--auto";
|
|
6983
|
+
/** 调度 tick 周期(10min——验证形态"一个就够") */
|
|
6984
|
+
const TICK_MS = 6e5;
|
|
6985
|
+
/** SESSION.md 内「下一轮动机」段标记(自生偏见载体——轨迹预设自己的未来) */
|
|
6986
|
+
const MOTIVATION_MARKER = "下一轮动机";
|
|
6987
|
+
/** 缺省避开的高峰时段(北京时间 [start, end) 不唤起——用量峰谷省钱) */
|
|
6988
|
+
const DEFAULT_AVOID_HOURS = {
|
|
6989
|
+
start: 8,
|
|
6990
|
+
end: 18
|
|
6991
|
+
};
|
|
6992
|
+
function readAutoTrajectorySettings(root) {
|
|
6993
|
+
return loadSerenityConfig(root).autotrajectory ?? null;
|
|
6994
|
+
}
|
|
7089
6995
|
/**
|
|
7090
|
-
*
|
|
7091
|
-
*
|
|
7092
|
-
* 定位:dsp **只提供工具与知识**,不向 CCC 自动安装任何东西(实验可能失败,不污染 CCC)——
|
|
7093
|
-
* 实验是 CCC 的自选动作:agent 调本工具(doc/全报告)即懂实验,init/random/check 辅助,
|
|
7094
|
-
* 实际执行(写配置/写偏见脚本/标记会话)由 CCC 自己决定、自己用现有工具完成。
|
|
7095
|
-
*
|
|
7096
|
-
* 实现:薄封装——exec 包内静态脚本(npm files 含 experiments/),脚本是单一真相源。
|
|
7097
|
-
* 环境注入 SERENITY_ROOT(当前 CCC 根)供脚本定位;bun 优先(可直跑 TS)。
|
|
6996
|
+
* 北京时间(UTC+8)当前小时——不依赖服务器时区(服务器可能 UTC/本地任意)。
|
|
7098
6997
|
*/
|
|
6998
|
+
function beijingHour(nowMs) {
|
|
6999
|
+
return Math.floor((nowMs + 288e5) % 864e5 / 36e5);
|
|
7000
|
+
}
|
|
7099
7001
|
/**
|
|
7100
|
-
*
|
|
7101
|
-
*
|
|
7102
|
-
* vitest 源码直跑时指向 src/tools/x.ts(src/tools → 包根 2 层)——逐级上溯查找,
|
|
7103
|
-
* 两种布局都稳(找到 experiments/autotrajectory/scripts/autotrajectory-exp.ts 即止)。
|
|
7002
|
+
* 唤起窗口判定:北京时间 [avoidStart, avoidEnd) 内不唤起(缺省 8~18)。
|
|
7003
|
+
* 单段避开(start<=end):窗口 = [0,start) ∪ [end,24);跨零点避开(start>end):窗口 = [end,start)。
|
|
7104
7004
|
*/
|
|
7105
|
-
function
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
if (parent === cur) return null;
|
|
7112
|
-
cur = parent;
|
|
7113
|
-
}
|
|
7005
|
+
function inAllowedWakeWindow(nowMs, avoid) {
|
|
7006
|
+
const start = avoid?.start ?? DEFAULT_AVOID_HOURS.start;
|
|
7007
|
+
const end = avoid?.end ?? DEFAULT_AVOID_HOURS.end;
|
|
7008
|
+
const h = beijingHour(nowMs);
|
|
7009
|
+
if (start <= end) return h < start || h >= end;
|
|
7010
|
+
return h >= end && h < start;
|
|
7114
7011
|
}
|
|
7115
|
-
/**
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
"all",
|
|
7119
|
-
"init",
|
|
7120
|
-
"random",
|
|
7121
|
-
"doc",
|
|
7122
|
-
"check",
|
|
7123
|
-
"status",
|
|
7124
|
-
"guide"
|
|
7125
|
-
];
|
|
7126
|
-
function agentCwd$1(exec) {
|
|
7127
|
-
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
7012
|
+
/** 标志位判定:SESSION.md 所在目录名以 --auto 结尾 → 自主轨迹形态 */
|
|
7013
|
+
function isAutoTrajectorySession(mdPath) {
|
|
7014
|
+
return basename(dirname(mdPath)).endsWith(AUTO_DIR_SUFFIX);
|
|
7128
7015
|
}
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7016
|
+
/**
|
|
7017
|
+
* 目标会话定位:**必须配置 cfg.session**(S###/目录名)——CCC 日常有多条 trajectory 在跑,
|
|
7018
|
+
* 绝不默认唤起(缺省最近活跃会误伤其他正在运行的轨迹;用户拍板:必须配置才生效)。
|
|
7019
|
+
* 未配置 session 或未命中 → null(不唤起)。
|
|
7020
|
+
*/
|
|
7021
|
+
function resolveTargetMd(root, cfg) {
|
|
7022
|
+
if (!cfg?.session) return null;
|
|
7023
|
+
const found = findSession(sessionsRoot(root), cfg.session);
|
|
7024
|
+
if (!found) return null;
|
|
7025
|
+
const md = join(found.path, "SESSION.md");
|
|
7026
|
+
return existsSync(md) ? md : null;
|
|
7134
7027
|
}
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7028
|
+
/**
|
|
7029
|
+
* 唤起条件(纯逻辑,可测):enabled + 未在运行 + 目录标志 + mtime 超间隔 + 窗口允许。
|
|
7030
|
+
*/
|
|
7031
|
+
function shouldWake(settings, mdPath, nowMs, running) {
|
|
7032
|
+
if (!settings?.enabled || running) return false;
|
|
7033
|
+
if (!mdPath || !isAutoTrajectorySession(mdPath)) return false;
|
|
7034
|
+
if (!inAllowedWakeWindow(nowMs, settings.avoidWakeHours)) return false;
|
|
7035
|
+
try {
|
|
7036
|
+
const mtime = statSync(mdPath).mtimeMs;
|
|
7037
|
+
const hours = Math.max(1, settings.intervalHours ?? 12);
|
|
7038
|
+
return nowMs - mtime >= hours * 36e5;
|
|
7039
|
+
} catch {
|
|
7040
|
+
return false;
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
/** 自生动机读取:SESSION.md「下一轮动机」段内容(到下一个二级标题或文件尾;无 → null) */
|
|
7044
|
+
function readSelfGeneratedMotivation(mdPath) {
|
|
7045
|
+
try {
|
|
7046
|
+
const content = readFileSync(mdPath, "utf-8");
|
|
7047
|
+
const idx = content.indexOf(MOTIVATION_MARKER);
|
|
7048
|
+
if (idx < 0) return null;
|
|
7049
|
+
const rest = content.slice(idx + 5);
|
|
7050
|
+
const nextHeading = rest.search(/\n## /);
|
|
7051
|
+
return (nextHeading >= 0 ? rest.slice(0, nextHeading) : rest).trim() || null;
|
|
7052
|
+
} catch {
|
|
7053
|
+
return null;
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
/**
|
|
7057
|
+
* 偏见内容:直接运行 CCC 根目录下偏见提供者脚本(biasProvider,缺省 autotrajectory-bias.ts)→ stdout。
|
|
7058
|
+
* 脚本缺失 → 返回 { text: null, error: 提示实现 }(唤起侧报错要求实现,不静默跳过)。
|
|
7059
|
+
* 路径逃逸校验(resolveInside);bun 优先,node 兜底;600s 超时。
|
|
7060
|
+
*/
|
|
7061
|
+
async function fetchBiasContent(root, providerRel) {
|
|
7062
|
+
let scriptAbs;
|
|
7063
|
+
try {
|
|
7064
|
+
scriptAbs = resolveInside(root, providerRel);
|
|
7065
|
+
} catch {
|
|
7066
|
+
return {
|
|
7067
|
+
text: null,
|
|
7068
|
+
error: `biasProvider 路径逃逸(须在 CCC 根内): ${providerRel}`
|
|
7069
|
+
};
|
|
7070
|
+
}
|
|
7071
|
+
if (!existsSync(scriptAbs)) return {
|
|
7072
|
+
text: null,
|
|
7073
|
+
error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(stdout 输出偏见内容一行;acc_msm exec autotrajectory-exp init 可生成模板)`
|
|
7074
|
+
};
|
|
7075
|
+
const runs = [["bun", [scriptAbs]], [process.execPath, [scriptAbs]]];
|
|
7076
|
+
for (const [cmd, args] of runs) try {
|
|
7077
|
+
const r = spawnSync(cmd, args, {
|
|
7163
7078
|
encoding: "utf-8",
|
|
7164
7079
|
timeout: 6e5,
|
|
7165
|
-
env,
|
|
7166
7080
|
stdio: [
|
|
7167
7081
|
"ignore",
|
|
7168
7082
|
"pipe",
|
|
7169
7083
|
"pipe"
|
|
7170
7084
|
]
|
|
7171
7085
|
});
|
|
7172
|
-
if (r.status === 0) {
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
}
|
|
7176
|
-
if (r.error?.code === "ENOENT")
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
});
|
|
7184
|
-
//#endregion
|
|
7185
|
-
//#region src/seams/keeper.ts
|
|
7186
|
-
const SCORES = {
|
|
7187
|
-
write: 3,
|
|
7188
|
-
edit: 3,
|
|
7189
|
-
str_replace_editor: 3,
|
|
7190
|
-
task: 10,
|
|
7191
|
-
read: 1,
|
|
7192
|
-
grep: 1,
|
|
7193
|
-
glob: 1,
|
|
7194
|
-
skill: 1,
|
|
7195
|
-
acc_msm: 1,
|
|
7196
|
-
cc_fs: 1
|
|
7197
|
-
};
|
|
7198
|
-
function scoreTool(toolName) {
|
|
7199
|
-
return SCORES[toolName] ?? 0;
|
|
7200
|
-
}
|
|
7201
|
-
var KeeperTracker = class {
|
|
7202
|
-
threshold;
|
|
7203
|
-
now;
|
|
7204
|
-
score = 0;
|
|
7205
|
-
lastTs = 0;
|
|
7206
|
-
counter = 0;
|
|
7207
|
-
constructor(threshold, now = Date.now) {
|
|
7208
|
-
this.threshold = threshold;
|
|
7209
|
-
this.now = now;
|
|
7210
|
-
}
|
|
7211
|
-
/** 记录一次工具调用 + 经过时间;返回是否应触发提醒 */
|
|
7212
|
-
step(toolName) {
|
|
7213
|
-
const now = this.now();
|
|
7214
|
-
if (this.lastTs > 0) this.score += Math.floor((now - this.lastTs) / 6e4);
|
|
7215
|
-
this.lastTs = now;
|
|
7216
|
-
this.score += scoreTool(toolName);
|
|
7217
|
-
return this.score >= this.threshold;
|
|
7218
|
-
}
|
|
7219
|
-
/** 生成确认码并清零积分 */
|
|
7220
|
-
ack() {
|
|
7221
|
-
this.counter += 1;
|
|
7222
|
-
this.score = 0;
|
|
7223
|
-
this.lastTs = 0;
|
|
7224
|
-
return `K${this.counter}`;
|
|
7225
|
-
}
|
|
7226
|
-
get currentScore() {
|
|
7227
|
-
return this.score;
|
|
7086
|
+
if (r.status === 0) return {
|
|
7087
|
+
text: (r.stdout ?? "").trim() || null,
|
|
7088
|
+
error: null
|
|
7089
|
+
};
|
|
7090
|
+
if (r.error?.code === "ENOENT") continue;
|
|
7091
|
+
return {
|
|
7092
|
+
text: null,
|
|
7093
|
+
error: `偏见内容提供者脚本执行失败(exit ${r.status ?? "?"}): ${r.stderr?.trim() || r.stdout?.trim() || ""}`
|
|
7094
|
+
};
|
|
7095
|
+
} catch {
|
|
7096
|
+
continue;
|
|
7228
7097
|
}
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7098
|
+
return {
|
|
7099
|
+
text: null,
|
|
7100
|
+
error: "偏见内容提供者脚本无法运行(bun 与 node 均不可用)"
|
|
7101
|
+
};
|
|
7102
|
+
}
|
|
7103
|
+
/** 唤起消息(三段式:身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
|
|
7104
|
+
function buildWakeMessage(opts) {
|
|
7105
|
+
const lines = [
|
|
7106
|
+
`[自主轨迹唤起] — 距上次轨迹活动已满 ${opts.intervalHours} 小时,自动继续。`,
|
|
7107
|
+
"",
|
|
7108
|
+
`身份锚定:继续 ${opts.sessionName} 的 trajectory(SESSION.md: ${opts.mdPath})。`,
|
|
7109
|
+
"先验偏见:"
|
|
7110
|
+
];
|
|
7111
|
+
if (opts.motivation) lines.push(` · 自生动机:${opts.motivation}`);
|
|
7112
|
+
if (opts.biasContent) lines.push(` · 偏见内容:${opts.biasContent}`);
|
|
7113
|
+
if (!opts.motivation && !opts.biasContent) lines.push(" · (无——本轮纯自主探索)");
|
|
7114
|
+
lines.push("", "任务:执行一轮自主认知(探索/反事实检验),把产出写入 SESSION.md", "「自主探索日志」段,并预写「下一轮动机」段。完成后自然结束。");
|
|
7115
|
+
return lines.join("\n");
|
|
7116
|
+
}
|
|
7117
|
+
async function performAutoTrajectoryWake(ctx, root, settings, opts = {}) {
|
|
7118
|
+
if (!settings?.enabled) return {
|
|
7119
|
+
ok: false,
|
|
7120
|
+
detail: "autotrajectory 未启用(enabled=false)"
|
|
7121
|
+
};
|
|
7122
|
+
const mdPath = resolveTargetMd(root, settings);
|
|
7123
|
+
if (!mdPath) return {
|
|
7124
|
+
ok: false,
|
|
7125
|
+
detail: "目标会话未命中(session 未配置或 AGENT_SESSIONS 无匹配)"
|
|
7126
|
+
};
|
|
7127
|
+
if (!isAutoTrajectorySession(mdPath)) return {
|
|
7128
|
+
ok: false,
|
|
7129
|
+
detail: "目标会话目录无 --auto 标志(AGENT_SESSIONS/<date>--<desc>--auto/)"
|
|
7130
|
+
};
|
|
7131
|
+
if (!opts.force) {
|
|
7132
|
+
const now = Date.now();
|
|
7133
|
+
if (!inAllowedWakeWindow(now, settings.avoidWakeHours)) return {
|
|
7134
|
+
ok: false,
|
|
7135
|
+
detail: "当前在北京高峰避开窗口内(不唤起)"
|
|
7136
|
+
};
|
|
7137
|
+
try {
|
|
7138
|
+
const mtime = statSync(mdPath).mtimeMs;
|
|
7139
|
+
const hours = Math.max(1, settings.intervalHours ?? 12);
|
|
7140
|
+
if (now - mtime < hours * 36e5) return {
|
|
7141
|
+
ok: false,
|
|
7142
|
+
detail: `距上次轨迹活动不足 ${hours}h(等待中)`
|
|
7143
|
+
};
|
|
7144
|
+
} catch {
|
|
7145
|
+
return {
|
|
7146
|
+
ok: false,
|
|
7147
|
+
detail: "SESSION.md 读取失败(statSync)"
|
|
7148
|
+
};
|
|
7149
|
+
}
|
|
7150
|
+
}
|
|
7151
|
+
const agent = resolveTargetAgent(ctx, mdPath);
|
|
7152
|
+
if (!agent) return {
|
|
7153
|
+
ok: false,
|
|
7154
|
+
detail: `目标会话 agent 不可得——${diagnoseTargetUnavailable(ctx, mdPath) ?? "目标会话未打开或命名未生效"}`
|
|
7155
|
+
};
|
|
7156
|
+
const provider = settings.biasProvider?.trim() || "autotrajectory-bias.ts";
|
|
7157
|
+
const biasRes = await fetchBiasContent(root, provider);
|
|
7158
|
+
if (biasRes.error) return {
|
|
7159
|
+
ok: false,
|
|
7160
|
+
detail: `偏见内容缺失:${biasRes.error}`
|
|
7161
|
+
};
|
|
7162
|
+
const motivation = readSelfGeneratedMotivation(mdPath);
|
|
7163
|
+
const message = buildWakeMessage({
|
|
7164
|
+
sessionName: basename(dirname(mdPath)),
|
|
7165
|
+
mdPath,
|
|
7166
|
+
intervalHours: Math.max(1, settings.intervalHours ?? 12),
|
|
7167
|
+
motivation,
|
|
7168
|
+
biasContent: biasRes.text
|
|
7169
|
+
});
|
|
7170
|
+
agent.steer(createUserMessage({
|
|
7171
|
+
content: [{
|
|
7172
|
+
type: "text",
|
|
7173
|
+
text: message
|
|
7174
|
+
}],
|
|
7175
|
+
source: PLUGIN_SOURCE$3
|
|
7176
|
+
}));
|
|
7177
|
+
return {
|
|
7178
|
+
ok: true,
|
|
7179
|
+
detail: `已唤起 ${basename(dirname(mdPath))}(偏见提供者 ${provider})`
|
|
7180
|
+
};
|
|
7181
|
+
}
|
|
7182
|
+
/**
|
|
7183
|
+
* 装配(index.ts apply 调用)。时钟唤起(v1.26.14 修复——用户"手动唤起正常,时钟唤起不工作"):
|
|
7184
|
+
*
|
|
7185
|
+
* 根因:旧实现启动时一次性 `resolveAutoTrajectoryRoot(ctx)`——web 进程启动时 live 会话
|
|
7186
|
+
* 往往为空(用户尚未打开实验 CCC 会话)→ root=null → settings=null → **定时器根本不启动**;
|
|
7187
|
+
* 手动唤起走 HTTP 端点(点击时重新解析 root)→ 正常。
|
|
7188
|
+
*
|
|
7189
|
+
* 修复:
|
|
7190
|
+
* ① **动态解析**:每次 tick 重新解析 root + settings(不绑定启动时值)——live 会话变化即跟上;
|
|
7191
|
+
* ② **事件驱动启动**:监听 `session/created`(live 会话出现)→ 启动定时器(进程启动时若
|
|
7192
|
+
* 已有实验 CCC 会话则立即启动);
|
|
7193
|
+
* ③ **优先实验 CCC**:resolveAutoTrajectoryCcc(配置了 autotrajectory 的 live CCC)优先于
|
|
7194
|
+
* 进程 cwd/任一 live(多 CCC 同实例时时钟唤起绑定实验 CCC,而非当前维护会话 CCC)。
|
|
7195
|
+
*
|
|
7196
|
+
* 零资源占用语义保留:未配置/未启用 → tick 内直接 return(定时器存在但每 10min 一次空检查,
|
|
7197
|
+
* unref 不阻塞进程退出)。
|
|
7198
|
+
*/
|
|
7199
|
+
function registerAutoTrajectory(ctx) {
|
|
7200
|
+
let timer = null;
|
|
7201
|
+
let running = false;
|
|
7202
|
+
/** 解析当前应绑定的实验 CCC:配置了 autotrajectory 的 live CCC 优先,回退进程 cwd/任一 live */
|
|
7203
|
+
const resolveRoot = () => resolveAutoTrajectoryCcc(ctx) ?? resolveAutoTrajectoryRoot(ctx);
|
|
7204
|
+
const tick = () => {
|
|
7205
|
+
if (running) return;
|
|
7206
|
+
const root = resolveRoot();
|
|
7207
|
+
const settings = root ? readAutoTrajectorySettings(root) : null;
|
|
7208
|
+
if (!settings?.enabled) return;
|
|
7209
|
+
if (!shouldWake(settings, resolveTargetMd(root, settings), Date.now(), running)) return;
|
|
7210
|
+
running = true;
|
|
7211
|
+
(async () => {
|
|
7212
|
+
try {
|
|
7213
|
+
const res = await performAutoTrajectoryWake(ctx, root, settings, { force: false });
|
|
7214
|
+
if (res.ok) console.log(`[serenity-hooks] ✓ 自主轨迹唤起(${res.detail})`);
|
|
7215
|
+
else console.warn(`[serenity-hooks] ✗ 自主轨迹唤起跳过:${res.detail}`);
|
|
7216
|
+
} catch (err) {
|
|
7217
|
+
console.warn(`[serenity-hooks] ✗ 自主轨迹唤起失败: ${String(err?.message ?? err)}`);
|
|
7218
|
+
} finally {
|
|
7219
|
+
running = false;
|
|
7220
|
+
}
|
|
7221
|
+
})();
|
|
7222
|
+
};
|
|
7223
|
+
const startTimer = () => {
|
|
7224
|
+
if (timer) return;
|
|
7225
|
+
const root = resolveRoot();
|
|
7226
|
+
const settings = root ? readAutoTrajectorySettings(root) : null;
|
|
7227
|
+
if (!settings?.enabled) return;
|
|
7228
|
+
timer = setInterval(tick, TICK_MS);
|
|
7229
|
+
timer.unref();
|
|
7230
|
+
console.log(`[serenity-hooks] ✓ 自主轨迹定时器启动(${root},intervalHours=${settings.intervalHours ?? 12})`);
|
|
7231
|
+
tick();
|
|
7232
|
+
};
|
|
7233
|
+
startTimer();
|
|
7234
|
+
try {
|
|
7235
|
+
ctx.on("session/created", () => startTimer());
|
|
7236
|
+
} catch {}
|
|
7237
|
+
}
|
|
7238
|
+
/** 目标 agent:从 SESSION.md 反向定位 dsh 会话(标题 F3 命名 S###-日期 匹配 + cwd 归属校验);不可得 → null */
|
|
7239
|
+
function resolveTargetAgent(ctx, mdPath) {
|
|
7240
|
+
const idMatch = basename(dirname(mdPath)).match(/--S(\d{3,})--/);
|
|
7241
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
7242
|
+
const targetRoot = findSerenityRoot(mdPath);
|
|
7243
|
+
try {
|
|
7244
|
+
const sessions = ctx.sessions;
|
|
7245
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7246
|
+
const cwd = s?.header?.cwd ?? "";
|
|
7247
|
+
if (targetRoot) {
|
|
7248
|
+
if (findSerenityRoot(cwd) !== targetRoot) continue;
|
|
7249
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
7250
|
+
const title = readSessionTitle(s);
|
|
7251
|
+
if (sid && title && (title === sid || title.startsWith(`${sid}-`))) {
|
|
7252
|
+
const agent = ctx.agents?.get?.(s.id ?? "");
|
|
7253
|
+
if (agent) return agent;
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
} catch {}
|
|
7257
|
+
return null;
|
|
7258
|
+
}
|
|
7259
|
+
/**
|
|
7260
|
+
* 从 dsh 会话 log 读取标题(latest-wins `session/title` 事件)——
|
|
7261
|
+
* **标题不在 sessions.list() 条目上**(wire/对象均无 title 字段;F3 命名经
|
|
7262
|
+
* sessionTitle.rename 写进 session log),必须从 events 提取(rebuild 同款读取模式)。
|
|
7263
|
+
*/
|
|
7264
|
+
function readSessionTitle(session) {
|
|
7265
|
+
try {
|
|
7266
|
+
const events = session?.events;
|
|
7267
|
+
if (!Array.isArray(events)) return null;
|
|
7268
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
7269
|
+
const e = events[i];
|
|
7270
|
+
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|
|
7271
|
+
}
|
|
7272
|
+
} catch {}
|
|
7273
|
+
return null;
|
|
7274
|
+
}
|
|
7275
|
+
/**
|
|
7276
|
+
* 诊断:目标会话 agent 为何不可得(供 performAutoTrajectoryWake 失败信息——
|
|
7277
|
+
* 区分"live 会话里无匹配" vs "匹配但 agent 未加载")。返回诊断文本(无则 null)。
|
|
7278
|
+
*/
|
|
7279
|
+
function diagnoseTargetUnavailable(ctx, mdPath) {
|
|
7280
|
+
const idMatch = basename(dirname(mdPath)).match(/--S(\d{3,})--/);
|
|
7281
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
7282
|
+
const targetRoot = findSerenityRoot(mdPath);
|
|
7283
|
+
const sameCccTitles = [];
|
|
7284
|
+
const agentMissing = { sid: false };
|
|
7285
|
+
try {
|
|
7286
|
+
const sessions = ctx.sessions;
|
|
7287
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7288
|
+
const cwd = s?.header?.cwd ?? "";
|
|
7289
|
+
if (targetRoot) {
|
|
7290
|
+
if (findSerenityRoot(cwd) !== targetRoot) continue;
|
|
7291
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
7292
|
+
const title = readSessionTitle(s);
|
|
7293
|
+
if (title) sameCccTitles.push(title);
|
|
7294
|
+
if (sid && title && (title === sid || title.startsWith(`${sid}-`))) {
|
|
7295
|
+
if (!ctx.agents?.get?.(s.id ?? "")) agentMissing.sid = true;
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
} catch {}
|
|
7299
|
+
if (agentMissing.sid) return `会话 ${sid} 已打开但 agent 未加载(会话可能刚创建/正在恢复——稍后重试)`;
|
|
7300
|
+
if (sameCccTitles.length > 0) return `目标 CCC 内 live 会话标题: [${sameCccTitles.join(", ")}]——均不匹配 ${sid}(目标会话未在 WebUI 打开,或命名未生效)`;
|
|
7301
|
+
return `目标 CCC 内无 live 会话(先在 WebUI 打开 ${sid} 会话后重试)`;
|
|
7302
|
+
}
|
|
7303
|
+
/** 自主轨迹绑定的 CCC 根:进程 cwd 上溯 .serenity 优先,回退任一 live 会话 root */
|
|
7304
|
+
function resolveAutoTrajectoryRoot(ctx) {
|
|
7305
|
+
const fromCwd = findSerenityRoot(process.cwd());
|
|
7306
|
+
if (fromCwd) return fromCwd;
|
|
7307
|
+
try {
|
|
7308
|
+
const sessions = ctx.sessions;
|
|
7309
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7310
|
+
const r = findSerenityRoot(s?.header?.cwd ?? "");
|
|
7311
|
+
if (r) return r;
|
|
7312
|
+
}
|
|
7313
|
+
} catch {}
|
|
7314
|
+
return null;
|
|
7315
|
+
}
|
|
7316
|
+
function getAutoTrajectoryStatus(root) {
|
|
7317
|
+
const cfg = readAutoTrajectorySettings(root);
|
|
7318
|
+
const now = Date.now();
|
|
7319
|
+
const base = {
|
|
7320
|
+
configured: cfg !== null,
|
|
7321
|
+
enabled: cfg?.enabled ?? false,
|
|
7322
|
+
intervalHours: Math.max(1, cfg?.intervalHours ?? 12),
|
|
7323
|
+
biasProvider: cfg?.biasProvider?.trim() || "autotrajectory-bias.ts",
|
|
7324
|
+
session: cfg?.session ?? null,
|
|
7325
|
+
avoidWakeHours: {
|
|
7326
|
+
start: cfg?.avoidWakeHours?.start ?? DEFAULT_AVOID_HOURS.start,
|
|
7327
|
+
end: cfg?.avoidWakeHours?.end ?? DEFAULT_AVOID_HOURS.end
|
|
7328
|
+
},
|
|
7329
|
+
beijingHour: beijingHour(now),
|
|
7330
|
+
windowAllowed: inAllowedWakeWindow(now, cfg?.avoidWakeHours)
|
|
7331
|
+
};
|
|
7332
|
+
if (!cfg) return {
|
|
7333
|
+
...base,
|
|
7334
|
+
target: null
|
|
7335
|
+
};
|
|
7336
|
+
const mdPath = resolveTargetMd(root, cfg);
|
|
7337
|
+
if (!mdPath) return {
|
|
7338
|
+
...base,
|
|
7339
|
+
target: null
|
|
7340
|
+
};
|
|
7341
|
+
let idleHours = 0;
|
|
7342
|
+
try {
|
|
7343
|
+
idleHours = (now - statSync(mdPath).mtimeMs) / 36e5;
|
|
7344
|
+
} catch {}
|
|
7345
|
+
return {
|
|
7346
|
+
...base,
|
|
7347
|
+
target: {
|
|
7348
|
+
dirName: basename(dirname(mdPath)),
|
|
7349
|
+
autoFlag: isAutoTrajectorySession(mdPath),
|
|
7350
|
+
idleHours: Math.max(0, idleHours),
|
|
7351
|
+
wakeable: shouldWake(cfg, mdPath, now, false)
|
|
7352
|
+
}
|
|
7353
|
+
};
|
|
7354
|
+
}
|
|
7355
|
+
/** 遍历 live 会话(sessions.list())+ 补标题(events session/title latest-wins)+ ccc 归属 */
|
|
7356
|
+
function listLiveSessions(ctx) {
|
|
7357
|
+
const out = [];
|
|
7358
|
+
try {
|
|
7359
|
+
const sessions = ctx.sessions;
|
|
7360
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
7361
|
+
const cwd = s?.header?.cwd ?? null;
|
|
7362
|
+
out.push({
|
|
7363
|
+
id: s?.id ?? "",
|
|
7364
|
+
cwd,
|
|
7365
|
+
cccRoot: cwd ? findSerenityRoot(cwd) : null,
|
|
7366
|
+
title: readSessionTitle(s)
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
7369
|
+
} catch {}
|
|
7370
|
+
return out;
|
|
7371
|
+
}
|
|
7372
|
+
/**
|
|
7373
|
+
* 面板解析:GET /serenity/autotrajectory 无 workspace 参数时——
|
|
7374
|
+
* **优先「配置了 autotrajectory 的 live CCC」**(用户实验状态所在;v1.26.14 修复:
|
|
7375
|
+
* 原 resolveWorkspace 解析到第一个 live 会话(如 home-serenity)→ 面板显示"未配置",
|
|
7376
|
+
* 用户实验 CCC(pangu)检测不到)。优先级:已启用 → 已配置(未启用)→ null。
|
|
7377
|
+
*/
|
|
7378
|
+
function resolveAutoTrajectoryCcc(ctx) {
|
|
7379
|
+
const enabled = [];
|
|
7380
|
+
const configured = [];
|
|
7381
|
+
for (const s of listLiveSessions(ctx)) {
|
|
7382
|
+
if (!s.cccRoot) continue;
|
|
7383
|
+
const cfg = readAutoTrajectorySettings(s.cccRoot);
|
|
7384
|
+
if (!cfg) continue;
|
|
7385
|
+
if (cfg.enabled) {
|
|
7386
|
+
if (!enabled.includes(s.cccRoot)) enabled.push(s.cccRoot);
|
|
7387
|
+
} else if (!configured.includes(s.cccRoot)) configured.push(s.cccRoot);
|
|
7388
|
+
}
|
|
7389
|
+
return enabled[0] ?? configured[0] ?? null;
|
|
7390
|
+
}
|
|
7391
|
+
function diagLive(ctx) {
|
|
7392
|
+
const liveSessions = listLiveSessions(ctx);
|
|
7393
|
+
const processCwd = process.cwd();
|
|
7394
|
+
const processCcc = findSerenityRoot(processCwd);
|
|
7395
|
+
const panelResolved = resolveAutoTrajectoryCcc(ctx);
|
|
7396
|
+
const autotrajectoryCccs = [];
|
|
7397
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7398
|
+
for (const s of liveSessions) {
|
|
7399
|
+
if (!s.cccRoot || seen.has(s.cccRoot)) continue;
|
|
7400
|
+
const cfg = readAutoTrajectorySettings(s.cccRoot);
|
|
7401
|
+
if (!cfg) continue;
|
|
7402
|
+
seen.add(s.cccRoot);
|
|
7403
|
+
const status = getAutoTrajectoryStatus(s.cccRoot);
|
|
7404
|
+
const mdPath = resolveTargetMd(s.cccRoot, cfg);
|
|
7405
|
+
const agentResolved = mdPath ? resolveTargetAgent(ctx, mdPath) !== null : false;
|
|
7406
|
+
const agentDiagnosis = mdPath && !agentResolved ? diagnoseTargetUnavailable(ctx, mdPath) : null;
|
|
7407
|
+
autotrajectoryCccs.push({
|
|
7408
|
+
root: s.cccRoot,
|
|
7409
|
+
enabled: status.enabled,
|
|
7410
|
+
session: status.session,
|
|
7411
|
+
target: status.target,
|
|
7412
|
+
agentResolved,
|
|
7413
|
+
agentDiagnosis
|
|
7414
|
+
});
|
|
7415
|
+
}
|
|
7416
|
+
return {
|
|
7417
|
+
processCwd,
|
|
7418
|
+
processCcc,
|
|
7419
|
+
liveSessions,
|
|
7420
|
+
autotrajectoryCccs,
|
|
7421
|
+
panelResolved
|
|
7422
|
+
};
|
|
7423
|
+
}
|
|
7424
|
+
//#endregion
|
|
7425
|
+
//#region src/tools/autotrajectory-exp.ts
|
|
7426
|
+
/**
|
|
7427
|
+
* autotrajectory-exp.ts — 自主轨迹实验一站式管理工具(v1.26.12 实验提案,默认关)
|
|
7428
|
+
*
|
|
7429
|
+
* 定位:dsp **只提供工具与知识**,不向 CCC 自动安装任何东西(实验可能失败,不污染 CCC)——
|
|
7430
|
+
* 实验是 CCC 的自选动作:agent 调本工具(doc/全报告)即懂实验,init/random/check 辅助,
|
|
7431
|
+
* 实际执行(写配置/写偏见脚本/标记会话)由 CCC 自己决定、自己用现有工具完成。
|
|
7432
|
+
*
|
|
7433
|
+
* 实现:薄封装——exec 包内静态脚本(npm files 含 experiments/),脚本是单一真相源。
|
|
7434
|
+
* 环境注入 SERENITY_ROOT(当前 CCC 根)供脚本定位;bun 优先(可直跑 TS)。
|
|
7435
|
+
* diag-live:**进程内诊断**(v1.26.14 用户"排查访问不到")——闭包捕获 ctx,
|
|
7436
|
+
* 直接读 live 会话/标题/agent(脚本 diag 看不到运行时),输出实例级诊断报告。
|
|
7437
|
+
*/
|
|
7438
|
+
/**
|
|
7439
|
+
* 定位包内实验脚本(npm files 分发 experiments/autotrajectory/)。
|
|
7440
|
+
* 布局差异:tsdown bundle 后 import.meta.url 指向 lib/index.js(lib → 包根 1 层);
|
|
7441
|
+
* vitest 源码直跑时指向 src/tools/x.ts(src/tools → 包根 2 层)——逐级上溯查找,
|
|
7442
|
+
* 两种布局都稳(找到 experiments/autotrajectory/scripts/autotrajectory-exp.ts 即止)。
|
|
7443
|
+
*/
|
|
7444
|
+
function findExpScript(startDir) {
|
|
7445
|
+
let cur = startDir;
|
|
7446
|
+
while (true) {
|
|
7447
|
+
const cand = join(cur, "experiments", "autotrajectory", "scripts", "autotrajectory-exp.ts");
|
|
7448
|
+
if (existsSync(cand)) return cand;
|
|
7449
|
+
const parent = dirname(cur);
|
|
7450
|
+
if (parent === cur) return null;
|
|
7451
|
+
cur = parent;
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
/** 包内实验脚本(上溯查找;找不到 → execute 报错提示包完整性) */
|
|
7455
|
+
const EXP_SCRIPT = findExpScript(dirname(fileURLToPath(import.meta.url)));
|
|
7456
|
+
const AUTO_TRAJECTORY_EXP_ACTIONS = [
|
|
7457
|
+
"all",
|
|
7458
|
+
"init",
|
|
7459
|
+
"random",
|
|
7460
|
+
"diag",
|
|
7461
|
+
"doc",
|
|
7462
|
+
"check",
|
|
7463
|
+
"status",
|
|
7464
|
+
"guide",
|
|
7465
|
+
"diag-live"
|
|
7466
|
+
];
|
|
7467
|
+
function agentCwd$1(exec) {
|
|
7468
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
7469
|
+
}
|
|
7470
|
+
function renderText(value) {
|
|
7471
|
+
return [{
|
|
7472
|
+
type: "text",
|
|
7473
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
7474
|
+
}];
|
|
7475
|
+
}
|
|
7476
|
+
/** 渲染 diag-live 报告为可读文本(EAP:结构清晰、命中点显式) */
|
|
7477
|
+
function renderDiagLive(r) {
|
|
7478
|
+
const lines = [
|
|
7479
|
+
"═══ 自主轨迹进程内诊断(diag-live,v1.26.14)═══",
|
|
7480
|
+
`进程 cwd: ${r.processCwd}(CCC: ${r.processCcc ?? "无 .serenity"})`,
|
|
7481
|
+
`面板解析(无参 GET 目标): ${r.panelResolved ?? "无配置 autotrajectory 的 live CCC"}`,
|
|
7482
|
+
"",
|
|
7483
|
+
`── live 会话(${r.liveSessions.length})──`
|
|
7484
|
+
];
|
|
7485
|
+
if (r.liveSessions.length === 0) lines.push(" (无 live 会话——WebUI 未打开任何会话;定时器绑定依赖 live 会话 cwd)");
|
|
7486
|
+
for (const s of r.liveSessions) lines.push(` · ${s.id}${s.title ? ` [${s.title}]` : " [无标题]"}${s.cwd ? ` cwd=${s.cwd}` : " cwd=无"}${s.cccRoot ? ` → CCC ${s.cccRoot}` : ""}`);
|
|
7487
|
+
lines.push("", `── 配置了 autotrajectory 的 CCC(${r.autotrajectoryCccs.length})──`);
|
|
7488
|
+
if (r.autotrajectoryCccs.length === 0) lines.push(" (无——live 会话中无任何 CCC 配置 autotrajectory)");
|
|
7489
|
+
for (const c of r.autotrajectoryCccs) {
|
|
7490
|
+
lines.push(` · ${c.root}(enabled=${c.enabled})`);
|
|
7491
|
+
lines.push(` session=${c.session ?? "未配置"} | 目标=${c.target ? `${c.target.dirName}(--auto ${c.target.autoFlag ? "✓" : "✗"},空闲 ${c.target.idleHours.toFixed(1)}h)` : "未命中"}`);
|
|
7492
|
+
lines.push(` agent 定位: ${c.agentResolved ? "✓ 可注入" : `✗ 不可得——${c.agentDiagnosis ?? "未知"}`}`);
|
|
7493
|
+
}
|
|
7494
|
+
return lines.join("\n");
|
|
7495
|
+
}
|
|
7496
|
+
/** 创建 autotrajectory-exp 工具(闭包捕获 ctx → diag-live 进程内诊断;v1.26.14) */
|
|
7497
|
+
function createAutoTrajectoryExpTool(ctx) {
|
|
7498
|
+
return defineTool({
|
|
7499
|
+
name: "autotrajectory-exp",
|
|
7500
|
+
description: "自主轨迹实验(Self-Sustaining Trajectory)一站式管理——实验提案 v1.26.14,默认关。无参/action=all:全报告(背景摘要 + 就绪检查 + 状态 + 下一步)——CCC agent 看一次即完整理解实验并知道怎么开始;init:初始化辅助(写配置 + 生成偏见提供者脚本模板);random:运行偏见提供者脚本输出当前偏见内容;diag:唤起条件链诊断(--ccc <path> 指定 CCC,无参递归扫描 /home/yh 两层——覆盖任意实验 CCC 位置;逐条件输出 + 阻断点 + 修复建议);diag-live:**进程内诊断**(v1.26.14——live 会话清单/标题/agent 定位/面板解析目标;排查\"面板检测不到实验 CCC\");doc:实验定义全文;check/status/guide:单项。实验是 CCC 的自选动作——dsp 只提供工具与知识,不自动安装任何东西。",
|
|
7501
|
+
parameters: { action: {
|
|
7502
|
+
type: "string",
|
|
7503
|
+
enum: [...AUTO_TRAJECTORY_EXP_ACTIONS],
|
|
7504
|
+
required: true,
|
|
7505
|
+
description: "Subcommand"
|
|
7506
|
+
} },
|
|
7507
|
+
output: {
|
|
7508
|
+
schema: { type: "json" },
|
|
7509
|
+
render: (_args, value) => renderText(value)
|
|
7510
|
+
},
|
|
7511
|
+
async execute(args, exec) {
|
|
7512
|
+
if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
|
|
7513
|
+
const root = findSerenityRoot(agentCwd$1(exec));
|
|
7514
|
+
const result = {};
|
|
7515
|
+
if (!EXP_SCRIPT) {
|
|
7516
|
+
result.error = "autotrajectory-exp 实验脚本未随安装分发(npm 包缺 experiments/autotrajectory/)——请检查包完整性";
|
|
7517
|
+
return result;
|
|
7518
|
+
}
|
|
7519
|
+
const script = EXP_SCRIPT;
|
|
7520
|
+
if (!existsSync(script)) {
|
|
7521
|
+
result.error = `autotrajectory-exp 脚本缺失(${script})——实验包未随安装分发,请检查 npm 包完整性`;
|
|
7522
|
+
return result;
|
|
7523
|
+
}
|
|
7524
|
+
const env = { ...process.env };
|
|
7525
|
+
if (root) env.SERENITY_ROOT = root;
|
|
7526
|
+
const r = spawnSync("bun", [script, args.action ?? "all"], {
|
|
7527
|
+
encoding: "utf-8",
|
|
7528
|
+
timeout: 6e5,
|
|
7529
|
+
env,
|
|
7530
|
+
stdio: [
|
|
7531
|
+
"ignore",
|
|
7532
|
+
"pipe",
|
|
7533
|
+
"pipe"
|
|
7534
|
+
]
|
|
7535
|
+
});
|
|
7536
|
+
if (r.status === 0) {
|
|
7537
|
+
result.output = r.stdout?.trim() || "(empty)";
|
|
7538
|
+
return result;
|
|
7539
|
+
}
|
|
7540
|
+
if (r.error?.code === "ENOENT") {
|
|
7541
|
+
result.error = "autotrajectory-exp 需要 bun 运行时(bun not found in PATH)";
|
|
7542
|
+
return result;
|
|
7543
|
+
}
|
|
7544
|
+
result.error = r.stderr?.trim() || r.stdout?.trim() || `exit ${r.status ?? "?"}`;
|
|
7545
|
+
return result;
|
|
7546
|
+
}
|
|
7547
|
+
});
|
|
7548
|
+
}
|
|
7549
|
+
//#endregion
|
|
7550
|
+
//#region src/seams/keeper.ts
|
|
7551
|
+
const SCORES = {
|
|
7552
|
+
write: 3,
|
|
7553
|
+
edit: 3,
|
|
7554
|
+
str_replace_editor: 3,
|
|
7555
|
+
task: 10,
|
|
7556
|
+
read: 1,
|
|
7557
|
+
grep: 1,
|
|
7558
|
+
glob: 1,
|
|
7559
|
+
skill: 1,
|
|
7560
|
+
acc_msm: 1,
|
|
7561
|
+
cc_fs: 1
|
|
7562
|
+
};
|
|
7563
|
+
function scoreTool(toolName) {
|
|
7564
|
+
return SCORES[toolName] ?? 0;
|
|
7565
|
+
}
|
|
7566
|
+
var KeeperTracker = class {
|
|
7567
|
+
threshold;
|
|
7568
|
+
now;
|
|
7569
|
+
score = 0;
|
|
7570
|
+
lastTs = 0;
|
|
7571
|
+
counter = 0;
|
|
7572
|
+
constructor(threshold, now = Date.now) {
|
|
7573
|
+
this.threshold = threshold;
|
|
7574
|
+
this.now = now;
|
|
7575
|
+
}
|
|
7576
|
+
/** 记录一次工具调用 + 经过时间;返回是否应触发提醒 */
|
|
7577
|
+
step(toolName) {
|
|
7578
|
+
const now = this.now();
|
|
7579
|
+
if (this.lastTs > 0) this.score += Math.floor((now - this.lastTs) / 6e4);
|
|
7580
|
+
this.lastTs = now;
|
|
7581
|
+
this.score += scoreTool(toolName);
|
|
7582
|
+
return this.score >= this.threshold;
|
|
7583
|
+
}
|
|
7584
|
+
/** 生成确认码并清零积分 */
|
|
7585
|
+
ack() {
|
|
7586
|
+
this.counter += 1;
|
|
7587
|
+
this.score = 0;
|
|
7588
|
+
this.lastTs = 0;
|
|
7589
|
+
return `K${this.counter}`;
|
|
7590
|
+
}
|
|
7591
|
+
get currentScore() {
|
|
7592
|
+
return this.score;
|
|
7593
|
+
}
|
|
7594
|
+
};
|
|
7595
|
+
function reminderText(code, score) {
|
|
7596
|
+
return `[TRAJECTORY-STEWARD] Score threshold reached (${score}). Please acknowledge with [TRAJECTORY-STEWARD-recorded-${code}] once progress is synced to the working session (acc-session show). No need to interrupt your work — just acknowledge inline and keep going.`;
|
|
7232
7597
|
}
|
|
7233
7598
|
/**
|
|
7234
7599
|
* F2 rebuild 提示(v1.21;v1.22.1 对齐"轨迹跟踪器"概念;v1.23.0 英化 + 载体关系;
|
|
@@ -7264,7 +7629,7 @@ function readContextPressure(ctx, session) {
|
|
|
7264
7629
|
return null;
|
|
7265
7630
|
}
|
|
7266
7631
|
}
|
|
7267
|
-
const PLUGIN_SOURCE$
|
|
7632
|
+
const PLUGIN_SOURCE$2 = {
|
|
7268
7633
|
kind: "plugin",
|
|
7269
7634
|
plugin: "dsh-serenity-hooks"
|
|
7270
7635
|
};
|
|
@@ -7287,7 +7652,8 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
7287
7652
|
};
|
|
7288
7653
|
ctx.on("tools/post-execute", async (exec, _result, next) => {
|
|
7289
7654
|
if (!exec.agent) return next();
|
|
7290
|
-
const
|
|
7655
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
7656
|
+
const root = findSerenityRoot(cwd);
|
|
7291
7657
|
if (!root) return next();
|
|
7292
7658
|
const sessionId = exec.agent?.session?.id;
|
|
7293
7659
|
const skiffKeeper = skiffTrajectoryEnabled(root, sessionId, "keeper");
|
|
@@ -7327,7 +7693,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
7327
7693
|
if (blocks.length === 0) return downstream;
|
|
7328
7694
|
const reminderAll = createUserMessage({
|
|
7329
7695
|
content: blocks,
|
|
7330
|
-
source: PLUGIN_SOURCE$
|
|
7696
|
+
source: PLUGIN_SOURCE$2
|
|
7331
7697
|
});
|
|
7332
7698
|
if (downstream.kind === "block") return {
|
|
7333
7699
|
kind: "block",
|
|
@@ -8338,7 +8704,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
8338
8704
|
}
|
|
8339
8705
|
return lines.join("\n");
|
|
8340
8706
|
}
|
|
8341
|
-
const PLUGIN_SOURCE$
|
|
8707
|
+
const PLUGIN_SOURCE$1 = {
|
|
8342
8708
|
kind: "plugin",
|
|
8343
8709
|
plugin: "dsh-serenity-hooks"
|
|
8344
8710
|
};
|
|
@@ -8355,7 +8721,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
8355
8721
|
}];
|
|
8356
8722
|
return createUserMessage({
|
|
8357
8723
|
content,
|
|
8358
|
-
source: PLUGIN_SOURCE$
|
|
8724
|
+
source: PLUGIN_SOURCE$1
|
|
8359
8725
|
});
|
|
8360
8726
|
}
|
|
8361
8727
|
/** 每 agent 是否已注入过(进程内存态) */
|
|
@@ -8397,7 +8763,8 @@ function registerContext(ctx, opts = {}) {
|
|
|
8397
8763
|
const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
|
|
8398
8764
|
const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
|
|
8399
8765
|
const seed = (agent) => {
|
|
8400
|
-
const
|
|
8766
|
+
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
8767
|
+
const root = findSerenityRoot(cwd);
|
|
8401
8768
|
if (!root) return;
|
|
8402
8769
|
const key = agentKey(agent);
|
|
8403
8770
|
if (isSkiffSessionId(key)) return;
|
|
@@ -8430,7 +8797,8 @@ function registerContext(ctx, opts = {}) {
|
|
|
8430
8797
|
});
|
|
8431
8798
|
if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
|
|
8432
8799
|
const { agent, messages } = payload;
|
|
8433
|
-
const
|
|
8800
|
+
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
8801
|
+
const root = findSerenityRoot(cwd);
|
|
8434
8802
|
const key = agentKey(agent);
|
|
8435
8803
|
const downstream = await next();
|
|
8436
8804
|
if (isSkiffSessionId(key)) return downstream;
|
|
@@ -8467,7 +8835,8 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
8467
8835
|
if (isSkiffSessionId(String(session.id ?? ""))) return;
|
|
8468
8836
|
const agent = ctx.agents.get(session.id);
|
|
8469
8837
|
if (!agent) return;
|
|
8470
|
-
const
|
|
8838
|
+
const cwd = agent.session?.header?.cwd ?? process.cwd();
|
|
8839
|
+
const root = findSerenityRoot(cwd);
|
|
8471
8840
|
if (!root) return;
|
|
8472
8841
|
try {
|
|
8473
8842
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
|
|
@@ -8483,6 +8852,7 @@ const FILE_UPLOAD_PATH = "/serenity/file-upload";
|
|
|
8483
8852
|
const CONFIG_PATH = "/serenity/config";
|
|
8484
8853
|
const CCCS_PATH = "/serenity/cccs";
|
|
8485
8854
|
const PUBLIC_ASK_PATH = "/serenity/public-ask";
|
|
8855
|
+
const AUTOTRAJECTORY_PATH = "/serenity/autotrajectory";
|
|
8486
8856
|
/** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
|
|
8487
8857
|
const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
|
|
8488
8858
|
const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -8621,10 +8991,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8621
8991
|
return;
|
|
8622
8992
|
}
|
|
8623
8993
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
8624
|
-
const
|
|
8994
|
+
const workspace = resolveWorkspace(ctx, {
|
|
8625
8995
|
sessionId: url.searchParams.get("sessionId") ?? void 0,
|
|
8626
8996
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
8627
|
-
})
|
|
8997
|
+
});
|
|
8998
|
+
const root = findSerenityRoot(workspace);
|
|
8628
8999
|
if (!root) {
|
|
8629
9000
|
sendJson$2(res, 200, { handymen: [] });
|
|
8630
9001
|
return;
|
|
@@ -8783,10 +9154,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8783
9154
|
return;
|
|
8784
9155
|
}
|
|
8785
9156
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
8786
|
-
const
|
|
9157
|
+
const workspace = resolveWorkspace(ctx, {
|
|
8787
9158
|
sessionId: url.searchParams.get("sessionId") ?? void 0,
|
|
8788
9159
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
8789
|
-
})
|
|
9160
|
+
});
|
|
9161
|
+
const root = findSerenityRoot(workspace) ?? "";
|
|
8790
9162
|
const { discoverCccs } = await Promise.resolve().then(() => skiff_debug_exports);
|
|
8791
9163
|
sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
|
|
8792
9164
|
} catch (err) {
|
|
@@ -8839,6 +9211,64 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
8839
9211
|
}
|
|
8840
9212
|
}
|
|
8841
9213
|
});
|
|
9214
|
+
ctx.webServer.register({
|
|
9215
|
+
kind: "exact",
|
|
9216
|
+
path: AUTOTRAJECTORY_PATH,
|
|
9217
|
+
handler: async (req, res) => {
|
|
9218
|
+
try {
|
|
9219
|
+
if (req.method === "POST") {
|
|
9220
|
+
if (req.headers["x-serenity-ui"] !== "1") {
|
|
9221
|
+
sendJson$2(res, 403, { error: "立即唤起仅限 WebUI(agent 不可自行唤起)" });
|
|
9222
|
+
return;
|
|
9223
|
+
}
|
|
9224
|
+
const raw = await readBody$3(req, 16384);
|
|
9225
|
+
const body = JSON.parse(raw);
|
|
9226
|
+
if (body.action !== "wake") {
|
|
9227
|
+
sendJson$2(res, 400, { error: "unsupported action (expected \"wake\")" });
|
|
9228
|
+
return;
|
|
9229
|
+
}
|
|
9230
|
+
const workspace = resolveWorkspace(ctx, {
|
|
9231
|
+
sessionId: body.sessionId,
|
|
9232
|
+
workspace: body.workspace
|
|
9233
|
+
});
|
|
9234
|
+
const root = findSerenityRoot(workspace);
|
|
9235
|
+
const effectiveRoot = !body.workspace && !body.sessionId ? (await Promise.resolve().then(() => autotrajectory_exports)).resolveAutoTrajectoryCcc(ctx) ?? root : root;
|
|
9236
|
+
if (!effectiveRoot) {
|
|
9237
|
+
sendJson$2(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
9238
|
+
return;
|
|
9239
|
+
}
|
|
9240
|
+
const { performAutoTrajectoryWake, readAutoTrajectorySettings } = await Promise.resolve().then(() => autotrajectory_exports);
|
|
9241
|
+
const settings = readAutoTrajectorySettings(effectiveRoot);
|
|
9242
|
+
if (!settings) {
|
|
9243
|
+
sendJson$2(res, 400, { error: "autotrajectory 未配置(.opencode/serenity.json 缺段)" });
|
|
9244
|
+
return;
|
|
9245
|
+
}
|
|
9246
|
+
const result = await performAutoTrajectoryWake(ctx, effectiveRoot, settings, { force: true });
|
|
9247
|
+
sendJson$2(res, result.ok ? 200 : 400, result);
|
|
9248
|
+
return;
|
|
9249
|
+
}
|
|
9250
|
+
if (req.method !== "GET") {
|
|
9251
|
+
sendJson$2(res, 405, { error: "method not allowed" });
|
|
9252
|
+
return;
|
|
9253
|
+
}
|
|
9254
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
9255
|
+
const workspace = resolveWorkspace(ctx, {
|
|
9256
|
+
sessionId: url.searchParams.get("sessionId") ?? void 0,
|
|
9257
|
+
workspace: url.searchParams.get("workspace") ?? void 0
|
|
9258
|
+
});
|
|
9259
|
+
const root = findSerenityRoot(workspace);
|
|
9260
|
+
const { getAutoTrajectoryStatus, resolveAutoTrajectoryCcc } = await Promise.resolve().then(() => autotrajectory_exports);
|
|
9261
|
+
const effectiveRoot = !url.searchParams.get("workspace") && !url.searchParams.get("sessionId") ? resolveAutoTrajectoryCcc(ctx) ?? root : root;
|
|
9262
|
+
if (!effectiveRoot) {
|
|
9263
|
+
sendJson$2(res, 200, { status: null });
|
|
9264
|
+
return;
|
|
9265
|
+
}
|
|
9266
|
+
sendJson$2(res, 200, { status: getAutoTrajectoryStatus(effectiveRoot) });
|
|
9267
|
+
} catch (err) {
|
|
9268
|
+
sendJson$2(res, 400, { error: err.message ?? String(err) });
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
});
|
|
8842
9272
|
}
|
|
8843
9273
|
//#endregion
|
|
8844
9274
|
//#region src/seams/env.ts
|
|
@@ -8932,7 +9362,8 @@ function registerOpencodeSkills(ctx) {
|
|
|
8932
9362
|
const provider = {
|
|
8933
9363
|
name: OPENCODE_PROVIDER,
|
|
8934
9364
|
async list(options) {
|
|
8935
|
-
const
|
|
9365
|
+
const cwd = options.cwd ?? process.cwd();
|
|
9366
|
+
const root = findSerenityRoot(cwd);
|
|
8936
9367
|
if (!root) return {
|
|
8937
9368
|
candidates: [],
|
|
8938
9369
|
complete: true
|
|
@@ -9884,7 +10315,7 @@ function buildRebuke(hits) {
|
|
|
9884
10315
|
const rebukeStates = /* @__PURE__ */ new Map();
|
|
9885
10316
|
//#endregion
|
|
9886
10317
|
//#region src/output-guard-seam.ts
|
|
9887
|
-
const PLUGIN_SOURCE
|
|
10318
|
+
const PLUGIN_SOURCE = {
|
|
9888
10319
|
kind: "plugin",
|
|
9889
10320
|
plugin: "dsh-serenity-hooks"
|
|
9890
10321
|
};
|
|
@@ -9915,7 +10346,8 @@ function registerOutputGuardHook(ctx) {
|
|
|
9915
10346
|
const agent = payload?.agent;
|
|
9916
10347
|
if (!agent) return;
|
|
9917
10348
|
if (!isExternalFaceSession(String(agent.session.id ?? ""))) return;
|
|
9918
|
-
const
|
|
10349
|
+
const cwd = agent.session.header?.cwd ?? process.cwd();
|
|
10350
|
+
const root = findSerenityRoot(cwd);
|
|
9919
10351
|
if (!root) return;
|
|
9920
10352
|
const id = agent.id;
|
|
9921
10353
|
const text = lastAssistantText(agent);
|
|
@@ -9940,7 +10372,7 @@ function registerOutputGuardHook(ctx) {
|
|
|
9940
10372
|
type: "text",
|
|
9941
10373
|
text: buildRebuke(hits)
|
|
9942
10374
|
}],
|
|
9943
|
-
source: PLUGIN_SOURCE
|
|
10375
|
+
source: PLUGIN_SOURCE
|
|
9944
10376
|
}));
|
|
9945
10377
|
} catch (error) {
|
|
9946
10378
|
console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
|
|
@@ -12837,227 +13269,6 @@ qInput.focus()
|
|
|
12837
13269
|
</html>`;
|
|
12838
13270
|
}
|
|
12839
13271
|
//#endregion
|
|
12840
|
-
//#region src/autotrajectory.ts
|
|
12841
|
-
const PLUGIN_SOURCE = {
|
|
12842
|
-
kind: "plugin",
|
|
12843
|
-
plugin: "dsh-serenity-hooks"
|
|
12844
|
-
};
|
|
12845
|
-
/** 自主轨迹会话目录后缀标志(用户拍板:后缀) */
|
|
12846
|
-
const AUTO_DIR_SUFFIX = "--auto";
|
|
12847
|
-
/** 调度 tick 周期(10min——验证形态"一个就够") */
|
|
12848
|
-
const TICK_MS = 6e5;
|
|
12849
|
-
/** SESSION.md 内「下一轮动机」段标记(自生偏见载体——轨迹预设自己的未来) */
|
|
12850
|
-
const MOTIVATION_MARKER = "下一轮动机";
|
|
12851
|
-
/** 缺省避开的高峰时段(北京时间 [start, end) 不唤起——用量峰谷省钱) */
|
|
12852
|
-
const DEFAULT_AVOID_HOURS = {
|
|
12853
|
-
start: 8,
|
|
12854
|
-
end: 18
|
|
12855
|
-
};
|
|
12856
|
-
function readAutoTrajectorySettings(root) {
|
|
12857
|
-
return loadSerenityConfig(root).autotrajectory ?? null;
|
|
12858
|
-
}
|
|
12859
|
-
/**
|
|
12860
|
-
* 北京时间(UTC+8)当前小时——不依赖服务器时区(服务器可能 UTC/本地任意)。
|
|
12861
|
-
*/
|
|
12862
|
-
function beijingHour(nowMs) {
|
|
12863
|
-
return Math.floor((nowMs + 288e5) % 864e5 / 36e5);
|
|
12864
|
-
}
|
|
12865
|
-
/**
|
|
12866
|
-
* 唤起窗口判定:北京时间 [avoidStart, avoidEnd) 内不唤起(缺省 8~18)。
|
|
12867
|
-
* 单段避开(start<=end):窗口 = [0,start) ∪ [end,24);跨零点避开(start>end):窗口 = [end,start)。
|
|
12868
|
-
*/
|
|
12869
|
-
function inAllowedWakeWindow(nowMs, avoid) {
|
|
12870
|
-
const start = avoid?.start ?? DEFAULT_AVOID_HOURS.start;
|
|
12871
|
-
const end = avoid?.end ?? DEFAULT_AVOID_HOURS.end;
|
|
12872
|
-
const h = beijingHour(nowMs);
|
|
12873
|
-
if (start <= end) return h < start || h >= end;
|
|
12874
|
-
return h >= end && h < start;
|
|
12875
|
-
}
|
|
12876
|
-
/** 标志位判定:SESSION.md 所在目录名以 --auto 结尾 → 自主轨迹形态 */
|
|
12877
|
-
function isAutoTrajectorySession(mdPath) {
|
|
12878
|
-
return basename(dirname(mdPath)).endsWith(AUTO_DIR_SUFFIX);
|
|
12879
|
-
}
|
|
12880
|
-
/**
|
|
12881
|
-
* 目标会话定位:**必须配置 cfg.session**(S###/目录名)——CCC 日常有多条 trajectory 在跑,
|
|
12882
|
-
* 绝不默认唤起(缺省最近活跃会误伤其他正在运行的轨迹;用户拍板:必须配置才生效)。
|
|
12883
|
-
* 未配置 session 或未命中 → null(不唤起)。
|
|
12884
|
-
*/
|
|
12885
|
-
function resolveTargetMd(root, cfg) {
|
|
12886
|
-
if (!cfg?.session) return null;
|
|
12887
|
-
const found = findSession(sessionsRoot(root), cfg.session);
|
|
12888
|
-
if (!found) return null;
|
|
12889
|
-
const md = join(found.path, "SESSION.md");
|
|
12890
|
-
return existsSync(md) ? md : null;
|
|
12891
|
-
}
|
|
12892
|
-
/**
|
|
12893
|
-
* 唤起条件(纯逻辑,可测):enabled + 未在运行 + 目录标志 + mtime 超间隔 + 窗口允许。
|
|
12894
|
-
*/
|
|
12895
|
-
function shouldWake(settings, mdPath, nowMs, running) {
|
|
12896
|
-
if (!settings?.enabled || running) return false;
|
|
12897
|
-
if (!mdPath || !isAutoTrajectorySession(mdPath)) return false;
|
|
12898
|
-
if (!inAllowedWakeWindow(nowMs, settings.avoidWakeHours)) return false;
|
|
12899
|
-
try {
|
|
12900
|
-
const mtime = statSync(mdPath).mtimeMs;
|
|
12901
|
-
const hours = Math.max(1, settings.intervalHours ?? 12);
|
|
12902
|
-
return nowMs - mtime >= hours * 36e5;
|
|
12903
|
-
} catch {
|
|
12904
|
-
return false;
|
|
12905
|
-
}
|
|
12906
|
-
}
|
|
12907
|
-
/** 自生动机读取:SESSION.md「下一轮动机」段内容(到下一个二级标题或文件尾;无 → null) */
|
|
12908
|
-
function readSelfGeneratedMotivation(mdPath) {
|
|
12909
|
-
try {
|
|
12910
|
-
const content = readFileSync(mdPath, "utf-8");
|
|
12911
|
-
const idx = content.indexOf(MOTIVATION_MARKER);
|
|
12912
|
-
if (idx < 0) return null;
|
|
12913
|
-
const rest = content.slice(idx + 5);
|
|
12914
|
-
const nextHeading = rest.search(/\n## /);
|
|
12915
|
-
return (nextHeading >= 0 ? rest.slice(0, nextHeading) : rest).trim() || null;
|
|
12916
|
-
} catch {
|
|
12917
|
-
return null;
|
|
12918
|
-
}
|
|
12919
|
-
}
|
|
12920
|
-
/**
|
|
12921
|
-
* 偏见内容:直接运行 CCC 根目录下偏见提供者脚本(biasProvider,缺省 autotrajectory-bias.ts)→ stdout。
|
|
12922
|
-
* 脚本缺失 → 返回 { text: null, error: 提示实现 }(唤起侧报错要求实现,不静默跳过)。
|
|
12923
|
-
* 路径逃逸校验(resolveInside);bun 优先,node 兜底;600s 超时。
|
|
12924
|
-
*/
|
|
12925
|
-
async function fetchBiasContent(root, providerRel) {
|
|
12926
|
-
let scriptAbs;
|
|
12927
|
-
try {
|
|
12928
|
-
scriptAbs = resolveInside(root, providerRel);
|
|
12929
|
-
} catch {
|
|
12930
|
-
return {
|
|
12931
|
-
text: null,
|
|
12932
|
-
error: `biasProvider 路径逃逸(须在 CCC 根内): ${providerRel}`
|
|
12933
|
-
};
|
|
12934
|
-
}
|
|
12935
|
-
if (!existsSync(scriptAbs)) return {
|
|
12936
|
-
text: null,
|
|
12937
|
-
error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(stdout 输出偏见内容一行;acc_msm exec autotrajectory-exp init 可生成模板)`
|
|
12938
|
-
};
|
|
12939
|
-
const runs = [["bun", [scriptAbs]], [process.execPath, [scriptAbs]]];
|
|
12940
|
-
for (const [cmd, args] of runs) try {
|
|
12941
|
-
const r = spawnSync(cmd, args, {
|
|
12942
|
-
encoding: "utf-8",
|
|
12943
|
-
timeout: 6e5,
|
|
12944
|
-
stdio: [
|
|
12945
|
-
"ignore",
|
|
12946
|
-
"pipe",
|
|
12947
|
-
"pipe"
|
|
12948
|
-
]
|
|
12949
|
-
});
|
|
12950
|
-
if (r.status === 0) return {
|
|
12951
|
-
text: (r.stdout ?? "").trim() || null,
|
|
12952
|
-
error: null
|
|
12953
|
-
};
|
|
12954
|
-
if (r.error?.code === "ENOENT") continue;
|
|
12955
|
-
return {
|
|
12956
|
-
text: null,
|
|
12957
|
-
error: `偏见内容提供者脚本执行失败(exit ${r.status ?? "?"}): ${r.stderr?.trim() || r.stdout?.trim() || ""}`
|
|
12958
|
-
};
|
|
12959
|
-
} catch {
|
|
12960
|
-
continue;
|
|
12961
|
-
}
|
|
12962
|
-
return {
|
|
12963
|
-
text: null,
|
|
12964
|
-
error: "偏见内容提供者脚本无法运行(bun 与 node 均不可用)"
|
|
12965
|
-
};
|
|
12966
|
-
}
|
|
12967
|
-
/** 唤起消息(三段式:身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
|
|
12968
|
-
function buildWakeMessage(opts) {
|
|
12969
|
-
const lines = [
|
|
12970
|
-
`[自主轨迹唤起] — 距上次轨迹活动已满 ${opts.intervalHours} 小时,自动继续。`,
|
|
12971
|
-
"",
|
|
12972
|
-
`身份锚定:继续 ${opts.sessionName} 的 trajectory(SESSION.md: ${opts.mdPath})。`,
|
|
12973
|
-
"先验偏见:"
|
|
12974
|
-
];
|
|
12975
|
-
if (opts.motivation) lines.push(` · 自生动机:${opts.motivation}`);
|
|
12976
|
-
if (opts.biasContent) lines.push(` · 偏见内容:${opts.biasContent}`);
|
|
12977
|
-
if (!opts.motivation && !opts.biasContent) lines.push(" · (无——本轮纯自主探索)");
|
|
12978
|
-
lines.push("", "任务:执行一轮自主认知(探索/反事实检验),把产出写入 SESSION.md", "「自主探索日志」段,并预写「下一轮动机」段。完成后自然结束。");
|
|
12979
|
-
return lines.join("\n");
|
|
12980
|
-
}
|
|
12981
|
-
/**
|
|
12982
|
-
* 装配(index.ts apply 调用):仅当 CCC 配置 autotrajectory.enabled=true 时启动定时器;
|
|
12983
|
-
* 否则零资源占用。定时器 tick 检查唤起条件 → 注入活跃会话(agent.steer,前台可见)。
|
|
12984
|
-
*/
|
|
12985
|
-
function registerAutoTrajectory(ctx) {
|
|
12986
|
-
const root = resolveAutoTrajectoryRoot(ctx);
|
|
12987
|
-
const settings = root ? readAutoTrajectorySettings(root) : null;
|
|
12988
|
-
if (!settings?.enabled) return;
|
|
12989
|
-
let running = false;
|
|
12990
|
-
const tick = () => {
|
|
12991
|
-
if (running) return;
|
|
12992
|
-
const mdPath = resolveTargetMd(root, settings);
|
|
12993
|
-
if (!shouldWake(settings, mdPath, Date.now(), running)) return;
|
|
12994
|
-
const agent = resolveTargetAgent(ctx, mdPath);
|
|
12995
|
-
if (!agent) {
|
|
12996
|
-
console.warn("[serenity-hooks] ✗ 自主轨迹唤起跳过:目标会话 agent 不可得(session use 激活后重试)");
|
|
12997
|
-
return;
|
|
12998
|
-
}
|
|
12999
|
-
running = true;
|
|
13000
|
-
(async () => {
|
|
13001
|
-
try {
|
|
13002
|
-
const provider = settings.biasProvider?.trim() || "autotrajectory-bias.ts";
|
|
13003
|
-
const biasRes = await fetchBiasContent(root, provider);
|
|
13004
|
-
if (biasRes.error) {
|
|
13005
|
-
console.error(`[serenity-hooks] ✗ 自主轨迹唤起中止(偏见内容缺失): ${biasRes.error}`);
|
|
13006
|
-
return;
|
|
13007
|
-
}
|
|
13008
|
-
const motivation = readSelfGeneratedMotivation(mdPath);
|
|
13009
|
-
const message = buildWakeMessage({
|
|
13010
|
-
sessionName: basename(dirname(mdPath)),
|
|
13011
|
-
mdPath,
|
|
13012
|
-
intervalHours: settings.intervalHours ?? 12,
|
|
13013
|
-
motivation,
|
|
13014
|
-
biasContent: biasRes.text
|
|
13015
|
-
});
|
|
13016
|
-
agent.steer(createUserMessage({
|
|
13017
|
-
content: [{
|
|
13018
|
-
type: "text",
|
|
13019
|
-
text: message
|
|
13020
|
-
}],
|
|
13021
|
-
source: PLUGIN_SOURCE
|
|
13022
|
-
}));
|
|
13023
|
-
console.log(`[serenity-hooks] ✓ 自主轨迹唤起(${basename(dirname(mdPath))},偏见提供者 ${provider})`);
|
|
13024
|
-
} catch (err) {
|
|
13025
|
-
console.warn(`[serenity-hooks] ✗ 自主轨迹唤起失败: ${String(err?.message ?? err)}`);
|
|
13026
|
-
} finally {
|
|
13027
|
-
running = false;
|
|
13028
|
-
}
|
|
13029
|
-
})();
|
|
13030
|
-
};
|
|
13031
|
-
setInterval(tick, TICK_MS).unref();
|
|
13032
|
-
tick();
|
|
13033
|
-
}
|
|
13034
|
-
/** 目标 agent:从 SESSION.md 反向定位 dsh 会话(标题 F3 命名 S###-日期 匹配);不可得 → null */
|
|
13035
|
-
function resolveTargetAgent(ctx, mdPath) {
|
|
13036
|
-
const idMatch = basename(dirname(mdPath)).match(/--S(\d{3,})--/);
|
|
13037
|
-
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
13038
|
-
try {
|
|
13039
|
-
const sessions = ctx.sessions;
|
|
13040
|
-
for (const s of sessions?.list?.() ?? []) if (sid && (s.title === sid || s.title?.startsWith(`${sid}-`))) {
|
|
13041
|
-
const agent = ctx.agents?.get?.(s.id ?? "");
|
|
13042
|
-
if (agent) return agent;
|
|
13043
|
-
}
|
|
13044
|
-
} catch {}
|
|
13045
|
-
return null;
|
|
13046
|
-
}
|
|
13047
|
-
/** 自主轨迹绑定的 CCC 根:进程 cwd 上溯 .serenity 优先,回退任一 live 会话 root */
|
|
13048
|
-
function resolveAutoTrajectoryRoot(ctx) {
|
|
13049
|
-
const fromCwd = findSerenityRoot(process.cwd());
|
|
13050
|
-
if (fromCwd) return fromCwd;
|
|
13051
|
-
try {
|
|
13052
|
-
const sessions = ctx.sessions;
|
|
13053
|
-
for (const s of sessions?.list?.() ?? []) {
|
|
13054
|
-
const r = findSerenityRoot(s?.header?.cwd ?? "");
|
|
13055
|
-
if (r) return r;
|
|
13056
|
-
}
|
|
13057
|
-
} catch {}
|
|
13058
|
-
return null;
|
|
13059
|
-
}
|
|
13060
|
-
//#endregion
|
|
13061
13272
|
//#region src/index.ts
|
|
13062
13273
|
const name = "dsh-serenity-hooks";
|
|
13063
13274
|
/** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在 */
|
|
@@ -13113,7 +13324,7 @@ function apply(ctx, config) {
|
|
|
13113
13324
|
ctx.tools.register(createRebuildTool(ctx));
|
|
13114
13325
|
ctx.tools.register(localstoreTool);
|
|
13115
13326
|
ctx.tools.register(skiffAdminTool);
|
|
13116
|
-
ctx.tools.register(
|
|
13327
|
+
ctx.tools.register(createAutoTrajectoryExpTool(ctx));
|
|
13117
13328
|
}
|
|
13118
13329
|
if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
|
|
13119
13330
|
if (config.keeper) registerKeeper(ctx, {
|