@shgroup/dsh-serenity-hooks 1.29.1 → 1.30.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 +6 -4
- package/dsh.plugin.json +9 -12
- package/experiments/autopilot-trajectory/SKILL.md +10 -10
- package/lib/client.js +2 -2
- package/lib/fs-ops.d.ts +1 -1
- package/lib/git-ops.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1554 -2127
- package/lib/invariant.d.ts +1 -1
- package/lib/invariant.js +7 -10
- package/lib/kit-ops.d.ts +4 -4
- package/lib/localstore-ops.d.ts +3 -3
- package/lib/msm-ops.d.ts +7 -7
- package/lib/output-guard-seam.d.ts +1 -1
- package/lib/rebuild.d.ts +13 -2
- package/lib/seams/guards.d.ts +7 -7
- package/lib/seams/keeper.d.ts +1 -1
- package/lib/seams/system-prompt.d.ts +1 -1
- package/lib/session-bound-p9oR9dj9.js +725 -0
- package/lib/session-bound.d.ts +1 -1
- package/lib/session-ops.d.ts +1 -1
- package/lib/skiff-core.d.ts +1 -1
- package/lib/{skiff-debug-CBU6T2F_.js → skiff-debug-BA8CDeJl.js} +2 -2
- package/lib/{skiff-role-BTdBHyOQ.js → skiff-role-LW4tjF9L.js} +4 -4
- package/lib/skiff-role.d.ts +2 -2
- package/lib/tools/cc-fs.d.ts +1 -1
- package/lib/tools/container-admin.d.ts +17 -0
- package/lib/tools/git.d.ts +1 -1
- package/lib/tools/kit.d.ts +1 -1
- package/lib/tools/msm.d.ts +19 -1
- package/lib/tools/praxis.d.ts +14 -0
- package/lib/tools/skiff-admin.d.ts +4 -4
- package/lib/{weixin-route-DFkRf9ou.js → weixin-route-jA0qENOf.js} +5 -5
- package/package.json +1 -1
- package/lib/tools/rebuild.d.ts +0 -19
package/lib/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
2
|
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CX48YNUL.js";
|
|
3
|
-
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-
|
|
4
|
-
import { C as splitModel, E as skiffRoleFor, S as requireWhitelistedModel, T as writeProgress, _ as buildRoundPrompt, a as startSkiffDebugServer, b as newStopToken, c as askSkiff, d as skiffMsmGate, f as skiffSessionInfo, g as HANDYMAN_GUIDE, h as unregisterSkiffSession, l as createSkiffAgent, m as skiffTrajectoryEnabled, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionSnapshot, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as getSkiffAgent, v as handymanProgressPaths, w as writeFailedStatus, x as readProgress, y as listActiveHandymen } from "./skiff-debug-
|
|
5
|
-
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-
|
|
3
|
+
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, l as systemPromptSource, r as isSkiffSessionId, s as roleToolWhitelist } from "./skiff-role-LW4tjF9L.js";
|
|
4
|
+
import { C as splitModel, E as skiffRoleFor, S as requireWhitelistedModel, T as writeProgress, _ as buildRoundPrompt, a as startSkiffDebugServer, b as newStopToken, c as askSkiff, d as skiffMsmGate, f as skiffSessionInfo, g as HANDYMAN_GUIDE, h as unregisterSkiffSession, l as createSkiffAgent, m as skiffTrajectoryEnabled, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffSessionSnapshot, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as getSkiffAgent, v as handymanProgressPaths, w as writeFailedStatus, x as readProgress, y as listActiveHandymen } from "./skiff-debug-BA8CDeJl.js";
|
|
5
|
+
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, h as runLocalStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-jA0qENOf.js";
|
|
6
|
+
import { C as useSession, S as summarize, _ as resolveSessionByTitle, a as archiveSessions, b as setActiveSessionInfo, c as extractSessionMdPathFromText, d as getActiveSessionInfo, f as healthCheck, g as readActiveSessionMd, h as qaCheck, i as SESSION_ACTIONS, l as findLatestActiveSessionMd, m as parseSessionContextFromEvents, n as readLastBound, o as closeSession, p as listSessions, r as DEFAULT_SESSION_SCOPE, s as createSession, t as appendBound, u as findSession, v as sessionEvents, x as showSession, y as sessionsRoot } from "./session-bound-p9oR9dj9.js";
|
|
6
7
|
import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-BfVxgCZy.js";
|
|
7
8
|
import { a as markdownToPlainText, c as sniffImageExt, i as getUpdates, n as downloadMedia, o as sendTextMessage, r as getConfig, s as sendTyping, t as TypingStatus } from "./weixin-api-OUCuw0Ws.js";
|
|
8
9
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -16,12 +17,12 @@ import { promisify } from "node:util";
|
|
|
16
17
|
import { platform as platform$1 } from "node:process";
|
|
17
18
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
18
19
|
import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
19
|
-
import { deriveEventMessage } from "@deepseek-ai/dsh-session";
|
|
20
20
|
import { createServer, request } from "node:http";
|
|
21
21
|
import * as zlib from "node:zlib";
|
|
22
|
+
import { deriveEventMessage } from "@deepseek-ai/dsh-session";
|
|
22
23
|
//#region src/fs-ops.ts
|
|
23
24
|
/**
|
|
24
|
-
* fs-ops.ts — cc_fs
|
|
25
|
+
* fs-ops.ts — container_fs 纯操作层(cc_fs → container_fs,v1.30;零 DSH 依赖,可独立单测)
|
|
25
26
|
*
|
|
26
27
|
* 行为对齐 osp(opencode-serenity-plugin/src/fs/file-system-tool.ts)——osp 是 ACC 工具 spec:
|
|
27
28
|
* - rm:目录需 recursive 才删除;非空目录无 recursive → [SKIP];保护 .serenity 与 CCC 根;dry-run 预览
|
|
@@ -112,8 +113,8 @@ function assertNotProtectedRegistry(root, absPath, targetLabel) {
|
|
|
112
113
|
const lower = (s) => process.platform === "win32" ? s.toLowerCase() : s;
|
|
113
114
|
const hit = isProtectedRegistryTarget(root, lower(rel));
|
|
114
115
|
if (hit === null) return;
|
|
115
|
-
if (hit.hit === "file") throw new Error(`
|
|
116
|
-
throw new Error(`
|
|
116
|
+
if (hit.hit === "file") throw new Error(`container-fs: refusing to directly modify mech-registry.json — use container_admin msm register/deregister instead`);
|
|
117
|
+
throw new Error(`container-fs: refusing to ${/rm|delete|remove/i.test(targetLabel) ? "remove" : "move"} "${rel}" — it is an ancestor of the ACC-managed mech-registry.json (${protectedRegistryTargets(root)?.fileRel}); the registry is managed by container_admin msm register/deregister`);
|
|
117
118
|
}
|
|
118
119
|
function validateWritePath(root, target) {
|
|
119
120
|
const absPath = target.startsWith("/") ? resolve(target) : resolveInside(root, target);
|
|
@@ -412,12 +413,12 @@ function runCcFs(root, args) {
|
|
|
412
413
|
//#endregion
|
|
413
414
|
//#region src/tools/cc-fs.ts
|
|
414
415
|
/**
|
|
415
|
-
* cc-fs.ts —
|
|
416
|
+
* cc-fs.ts — container_fs 真实 DSH 工具定义(defineTool)(v1.30:cc_fs → container_fs)
|
|
416
417
|
*
|
|
417
418
|
* 进程内注册(取代 v0.1 的 bash spawn runner):zod/schemastery 参数校验、
|
|
418
419
|
* 规范 JSON 输出、纯 render 投影。逻辑在 fs-ops.ts(可单测)。
|
|
419
420
|
*/
|
|
420
|
-
function agentCwd$
|
|
421
|
+
function agentCwd$9(exec) {
|
|
421
422
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
422
423
|
}
|
|
423
424
|
function renderText$12(value) {
|
|
@@ -427,8 +428,8 @@ function renderText$12(value) {
|
|
|
427
428
|
}];
|
|
428
429
|
}
|
|
429
430
|
const ccFsTool = defineTool({
|
|
430
|
-
name: "
|
|
431
|
-
description: "
|
|
431
|
+
name: "container_fs",
|
|
432
|
+
description: "Container filesystem operations (the CCC's hull — file operations confined to the container root; path escape blocked automatically). 15 subcommands: root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find. reveal opens a path in the OS file manager (Linux xdg-open / macOS Finder / Windows Explorer). Complements read/write/edit (structural operations).",
|
|
432
433
|
parameters: {
|
|
433
434
|
action: {
|
|
434
435
|
type: "string",
|
|
@@ -495,7 +496,7 @@ const ccFsTool = defineTool({
|
|
|
495
496
|
render: (args, value) => renderText$12(value)
|
|
496
497
|
},
|
|
497
498
|
async execute(args, exec) {
|
|
498
|
-
const root = findSerenityRoot(agentCwd$
|
|
499
|
+
const root = findSerenityRoot(agentCwd$9(exec));
|
|
499
500
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
500
501
|
return runCcFs(root, args);
|
|
501
502
|
}
|
|
@@ -530,12 +531,12 @@ const WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
530
531
|
"write",
|
|
531
532
|
"edit",
|
|
532
533
|
"str_replace_editor",
|
|
533
|
-
"
|
|
534
|
+
"container_fs",
|
|
534
535
|
"bash",
|
|
535
536
|
"append",
|
|
536
537
|
"touch"
|
|
537
538
|
]);
|
|
538
|
-
/**
|
|
539
|
+
/** container_fs 的写类子命令(mkdir/rm/mv/cp/touch/append);其余 9 子命令(root/resolve/exists/
|
|
539
540
|
* list/tree/relative/reveal/info/find)为只读——只读子命令不查黑名单(同 read 语义) */
|
|
540
541
|
const CC_FS_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
541
542
|
"mkdir",
|
|
@@ -547,7 +548,7 @@ const CC_FS_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
547
548
|
]);
|
|
548
549
|
/**
|
|
549
550
|
* 凭据文件硬名单(数据面守卫,v1.26.3,S142 用户:localstore.json 需要在 read 工具的黑名单里):
|
|
550
|
-
* **任何工具**(含 read/grep/glob/
|
|
551
|
+
* **任何工具**(含 read/grep/glob/container_fs 只读子命令)命中这些相对 CCC 根的路径 → deny。
|
|
551
552
|
*
|
|
552
553
|
* 与 safeMode.blacklist(写黑名单,v1.18.5 决策只拦写)语义独立:
|
|
553
554
|
* - 黑名单 = 用户可配置的写保护(REPOSITORIES/ 等只读参考源不误伤读)
|
|
@@ -558,7 +559,7 @@ const CC_FS_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
558
559
|
const SENSITIVE_CREDENTIAL_FILES = /* @__PURE__ */ new Set(["localstore.json"]);
|
|
559
560
|
/**
|
|
560
561
|
* MSM 注册表写保护(需求⑤b S142 用户拍板:"msm注册的文件需要写保护起来,避免CCC意外写坏搞崩自己"):
|
|
561
|
-
* mech-registry.json 是 CCC 执行层地基(坏 → loadMsmEntries 抛 →
|
|
562
|
+
* mech-registry.json 是 CCC 执行层地基(坏 → loadMsmEntries 抛 → container_admin/output-guard 全崩,
|
|
562
563
|
* register 也崩 → 自锁)。**写 deny、读 allow**(与 localstore 读 deny 语义区分 R6——注册表是
|
|
563
564
|
* 结构核心不是秘密,需被 output-guard/skiff-admin 读取建 MSM 词表)。
|
|
564
565
|
*
|
|
@@ -566,8 +567,8 @@ const SENSITIVE_CREDENTIAL_FILES = /* @__PURE__ */ new Set(["localstore.json"]);
|
|
|
566
567
|
* `.opencode/skills/<cccName>/references/mech-registry.json`(cccName = .serenity 首行)。
|
|
567
568
|
* 历史 root 级 `mech-registry.json` 与各 skill 分散注册表已废弃:**不再保护**
|
|
568
569
|
* (review P1:保护一个永不被读的文件 = 死锁——MSM 既不可见又不可删/迁移)。
|
|
569
|
-
* 写工具命中聚合档 → deny(唯一合法写通道 =
|
|
570
|
-
*
|
|
570
|
+
* 写工具命中聚合档 → deny(唯一合法写通道 = container_admin msm register/deregister
|
|
571
|
+
* 内部 writeRegistry,走工具实现不经 pre-execute → 天然豁免);读工具放行。
|
|
571
572
|
*/
|
|
572
573
|
function isProtectedRegistryRel(root, rel) {
|
|
573
574
|
const lower = (s) => process.platform === "win32" ? s.toLowerCase() : s;
|
|
@@ -584,10 +585,10 @@ function isProtectedRegistryRel(root, rel) {
|
|
|
584
585
|
} catch {}
|
|
585
586
|
return false;
|
|
586
587
|
}
|
|
587
|
-
/** 判定工具是否为写类:普通工具按名;
|
|
588
|
+
/** 判定工具是否为写类:普通工具按名;container_fs 复合工具按子命令 action */
|
|
588
589
|
function isWriteTool(toolName, action) {
|
|
589
590
|
if (!WRITE_TOOLS.has(toolName)) return false;
|
|
590
|
-
if (toolName === "
|
|
591
|
+
if (toolName === "container_fs") return action !== void 0 && CC_FS_WRITE_ACTIONS.has(action);
|
|
591
592
|
return true;
|
|
592
593
|
}
|
|
593
594
|
/**
|
|
@@ -628,7 +629,7 @@ function decideGuard(input) {
|
|
|
628
629
|
kind: "deny"
|
|
629
630
|
};
|
|
630
631
|
if (isProtectedRegistryRel(root, rel)) return {
|
|
631
|
-
deny: rel.endsWith("/mech-registry.json") ? `mech-registry.json is ACC-managed (${rel}) — use
|
|
632
|
+
deny: rel.endsWith("/mech-registry.json") ? `mech-registry.json is ACC-managed (${rel}) — use container_admin msm register/deregister instead of writing it directly` : `"${rel}" is an ancestor of the ACC-managed mech-registry.json — removing/moving it would destroy the registry; the registry is managed by container_admin msm register/deregister`,
|
|
632
633
|
kind: "deny"
|
|
633
634
|
};
|
|
634
635
|
const hit = matchBlacklist(rel, blacklist);
|
|
@@ -706,7 +707,7 @@ function syncSafeModeRestriction(agent, root) {
|
|
|
706
707
|
}
|
|
707
708
|
/**
|
|
708
709
|
* 从 exec 参数中提取常见路径字段(write/edit 工具);宽松读取。
|
|
709
|
-
*
|
|
710
|
+
* container_fs 复合工具:path(单路径)/ paths(数组)/ src / dst——取第一个命中(越界检查
|
|
710
711
|
* 逐路径由 decideGuard 单 pathArg 覆盖;多路径字段取首个,避免漏检主体路径)。
|
|
711
712
|
*/
|
|
712
713
|
function extractPathArg(exec) {
|
|
@@ -726,7 +727,7 @@ function extractPathArg(exec) {
|
|
|
726
727
|
const paths = a["paths"];
|
|
727
728
|
if (Array.isArray(paths) && paths.length > 0 && typeof paths[0] === "string") return paths[0];
|
|
728
729
|
}
|
|
729
|
-
/** 从 exec 参数提取
|
|
730
|
+
/** 从 exec 参数提取 container_fs 子命令 action;无则 undefined */
|
|
730
731
|
function extractAction(exec) {
|
|
731
732
|
const args = exec.arguments;
|
|
732
733
|
if (args === null || typeof args !== "object") return void 0;
|
|
@@ -833,7 +834,7 @@ function setSafeMode(root, on) {
|
|
|
833
834
|
//#endregion
|
|
834
835
|
//#region src/kit-ops.ts
|
|
835
836
|
/**
|
|
836
|
-
* kit-ops.ts — acc_kit
|
|
837
|
+
* kit-ops.ts — dashboard 纯操作层(acc_kit → dashboard,v1.30;零 DSH 依赖)
|
|
837
838
|
*
|
|
838
839
|
* 行为对齐 osp(opencode-serenity-plugin/src/acc-kit.ts)——osp 是 ACC 工具 spec:
|
|
839
840
|
* - health:{ccc, root, version, status: healthy|degraded, principles: {P1_rooted, P2_git_managed, P3_binary_permissions}}
|
|
@@ -990,9 +991,9 @@ async function runKit(root, args) {
|
|
|
990
991
|
//#endregion
|
|
991
992
|
//#region src/tools/kit.ts
|
|
992
993
|
/**
|
|
993
|
-
* kit.ts —
|
|
994
|
+
* kit.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
|
|
994
995
|
*/
|
|
995
|
-
function agentCwd$
|
|
996
|
+
function agentCwd$8(exec) {
|
|
996
997
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
997
998
|
}
|
|
998
999
|
function renderText$11(value) {
|
|
@@ -1002,8 +1003,8 @@ function renderText$11(value) {
|
|
|
1002
1003
|
}];
|
|
1003
1004
|
}
|
|
1004
1005
|
const kitTool = defineTool({
|
|
1005
|
-
name: "
|
|
1006
|
-
description: "
|
|
1006
|
+
name: "dashboard",
|
|
1007
|
+
description: "Dashboard — always-on container instruments (works any time, no side effects): health (CCC three-principle check P1/P2/config + MSM registry integrity report, healthy/degraded) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
|
|
1007
1008
|
parameters: {
|
|
1008
1009
|
action: {
|
|
1009
1010
|
type: "string",
|
|
@@ -1021,13 +1022,13 @@ const kitTool = defineTool({
|
|
|
1021
1022
|
render: (args, value) => renderText$11(value)
|
|
1022
1023
|
},
|
|
1023
1024
|
async execute(args, exec) {
|
|
1024
|
-
return await runKit(findSerenityRoot(agentCwd$
|
|
1025
|
+
return await runKit(findSerenityRoot(agentCwd$8(exec)), args);
|
|
1025
1026
|
}
|
|
1026
1027
|
});
|
|
1027
1028
|
//#endregion
|
|
1028
1029
|
//#region src/git-ops.ts
|
|
1029
1030
|
/**
|
|
1030
|
-
* git-ops.ts — cc_git
|
|
1031
|
+
* git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
|
|
1031
1032
|
*
|
|
1032
1033
|
* 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
|
|
1033
1034
|
* - status:JSON {clean, files:[{status,file}], summary}
|
|
@@ -1078,7 +1079,7 @@ function getCurrentBranch(root) {
|
|
|
1078
1079
|
"--abbrev-ref",
|
|
1079
1080
|
"HEAD"
|
|
1080
1081
|
]);
|
|
1081
|
-
if (!stdout) throw new Error("
|
|
1082
|
+
if (!stdout) throw new Error("container_git: cannot determine current branch");
|
|
1082
1083
|
return stdout;
|
|
1083
1084
|
}
|
|
1084
1085
|
function hasChanges(root) {
|
|
@@ -1117,21 +1118,21 @@ function runGit(root, args) {
|
|
|
1117
1118
|
case "commit": {
|
|
1118
1119
|
const ls = checkLocalstoreGitCompliance(root);
|
|
1119
1120
|
if (!ls.ok) throw new Error(ls.reason);
|
|
1120
|
-
if (!args.message || args.message.trim() === "") throw new Error("
|
|
1121
|
+
if (!args.message || args.message.trim() === "") throw new Error("container_git commit: missing required arg \"message\"");
|
|
1121
1122
|
if (!hasChanges(root)) return "(nothing to commit — working tree clean)";
|
|
1122
1123
|
const addResult = git(root, ["add", "-A"]);
|
|
1123
|
-
if (addResult.stderr) throw new Error(`
|
|
1124
|
+
if (addResult.stderr) throw new Error(`container_git commit: git add failed\n${addResult.stderr}`);
|
|
1124
1125
|
const commitResult = git(root, [
|
|
1125
1126
|
"commit",
|
|
1126
1127
|
"-m",
|
|
1127
1128
|
args.message
|
|
1128
1129
|
]);
|
|
1129
1130
|
if (commitResult.stderr && commitResult.stderr.includes("nothing to commit")) return "(nothing to commit — working tree clean)";
|
|
1130
|
-
if (commitResult.stderr && !commitResult.stdout) throw new Error(`
|
|
1131
|
+
if (commitResult.stderr && !commitResult.stdout) throw new Error(`container_git commit: git commit failed\n${commitResult.stderr}`);
|
|
1131
1132
|
return (commitResult.stdout || commitResult.stderr || "committed").trimEnd();
|
|
1132
1133
|
}
|
|
1133
1134
|
case "push": {
|
|
1134
|
-
if (!hasRemote(root)) throw new Error("
|
|
1135
|
+
if (!hasRemote(root)) throw new Error("container_git push: no remote configured. Add one with:\n git remote add origin <url>");
|
|
1135
1136
|
const branch = getCurrentBranch(root);
|
|
1136
1137
|
try {
|
|
1137
1138
|
git(root, [
|
|
@@ -1145,12 +1146,12 @@ function runGit(root, args) {
|
|
|
1145
1146
|
"origin",
|
|
1146
1147
|
branch
|
|
1147
1148
|
]);
|
|
1148
|
-
if (stderr.includes("non-fast-forward") || stderr.includes("rejected") || stderr.includes("[rejected]")) return `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again:
|
|
1149
|
+
if (stderr.includes("non-fast-forward") || stderr.includes("rejected") || stderr.includes("[rejected]")) return `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local branch is behind. Suggested actions:\n 1. Use bash: git fetch origin ${branch}\n 2. Inspect remote changes: git log HEAD..origin/${branch}\n 3. Merge or rebase: git merge origin/${branch} or git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add ... && git commit\n 5. Push again: container_git push`;
|
|
1149
1150
|
if (!stderr || stderr.includes("->") || stderr === "") return stdout || `Pushed to origin/${branch}`;
|
|
1150
|
-
throw new Error(`
|
|
1151
|
+
throw new Error(`container_git push failed:\n${stderr}`);
|
|
1151
1152
|
}
|
|
1152
1153
|
case "pull": {
|
|
1153
|
-
if (!hasRemote(root)) throw new Error("
|
|
1154
|
+
if (!hasRemote(root)) throw new Error("container_git pull: no remote configured. Add one with:\n git remote add origin <url>");
|
|
1154
1155
|
const branch = getCurrentBranch(root);
|
|
1155
1156
|
const fetchResult = git(root, [
|
|
1156
1157
|
"fetch",
|
|
@@ -1174,8 +1175,8 @@ function runGit(root, args) {
|
|
|
1174
1175
|
const msg = mergeResult.stdout || "Pulled successfully.";
|
|
1175
1176
|
return msg.endsWith("\n") ? msg.trimEnd() : msg;
|
|
1176
1177
|
}
|
|
1177
|
-
if (mergeResult.stderr.includes("non-fast-forward") || mergeResult.stderr.includes("Not possible to fast-forward") || mergeResult.stderr.includes("rejected") || mergeResult.stderr.includes("could not be applied")) return `[REJECTED] Pull from origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local history diverged from remote (non-fast-forward). Suggested actions:\n 1. Inspect differences:
|
|
1178
|
-
throw new Error(`
|
|
1178
|
+
if (mergeResult.stderr.includes("non-fast-forward") || mergeResult.stderr.includes("Not possible to fast-forward") || mergeResult.stderr.includes("rejected") || mergeResult.stderr.includes("could not be applied")) return `[REJECTED] Pull from origin/${branch} was rejected (non-fast-forward).\n\nRemote has new commits and your local history diverged from remote (non-fast-forward). Suggested actions:\n 1. Inspect differences: container_git log HEAD..origin/${branch}\n 2. Merge manually with bash: git merge origin/${branch}\n 3. Or rebase: git rebase origin/${branch}\n 4. Resolve conflicts manually if any: git add <file> && git commit\n 5. Push: container_git push`;
|
|
1179
|
+
throw new Error(`container_git pull failed:\n${mergeResult.stderr}`);
|
|
1179
1180
|
}
|
|
1180
1181
|
case "log": {
|
|
1181
1182
|
const n = Math.min(Math.max(args.count ?? 10, 1), 100);
|
|
@@ -1207,9 +1208,9 @@ function runGit(root, args) {
|
|
|
1207
1208
|
//#endregion
|
|
1208
1209
|
//#region src/tools/git.ts
|
|
1209
1210
|
/**
|
|
1210
|
-
* git.ts —
|
|
1211
|
+
* git.ts — container_git 真实 DSH 工具定义(defineTool)(v1.30:cc_git → container_git)
|
|
1211
1212
|
*/
|
|
1212
|
-
function agentCwd$
|
|
1213
|
+
function agentCwd$7(exec) {
|
|
1213
1214
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1214
1215
|
}
|
|
1215
1216
|
function renderText$10(value) {
|
|
@@ -1219,7 +1220,7 @@ function renderText$10(value) {
|
|
|
1219
1220
|
}];
|
|
1220
1221
|
}
|
|
1221
1222
|
const gitTool = defineTool({
|
|
1222
|
-
name: "
|
|
1223
|
+
name: "container_git",
|
|
1223
1224
|
description: "CCC git operations (cc-git semantics, aligned with osp). status/commit/push/log/pull/diff; non-fast-forward push/pull outputs [REJECTED] + suggested actions (never auto-force). merge/rebase/conflict resolution go through bash.",
|
|
1224
1225
|
parameters: {
|
|
1225
1226
|
action: {
|
|
@@ -1254,7 +1255,7 @@ const gitTool = defineTool({
|
|
|
1254
1255
|
render: (args, value) => renderText$10(value)
|
|
1255
1256
|
},
|
|
1256
1257
|
async execute(args, exec) {
|
|
1257
|
-
const root = findSerenityRoot(agentCwd$
|
|
1258
|
+
const root = findSerenityRoot(agentCwd$7(exec));
|
|
1258
1259
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1259
1260
|
return runGit(root, args);
|
|
1260
1261
|
}
|
|
@@ -1262,9 +1263,9 @@ const gitTool = defineTool({
|
|
|
1262
1263
|
//#endregion
|
|
1263
1264
|
//#region src/msm-ops.ts
|
|
1264
1265
|
/**
|
|
1265
|
-
* msm-ops.ts — acc_msm
|
|
1266
|
+
* msm-ops.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
|
|
1266
1267
|
*
|
|
1267
|
-
* MSM(Mech & Semi-Mech)框架:
|
|
1268
|
+
* MSM(Mech & Semi-Mech)框架:exec 执行 / register|deregister|check 管理。
|
|
1268
1269
|
* 复用 CCC 的 mech-registry.json(v1 或数组格式)。cwd 钉在 CCC 根。
|
|
1269
1270
|
*/
|
|
1270
1271
|
const execFileAsync = promisify(execFile);
|
|
@@ -1323,38 +1324,28 @@ function buildMsmEnv(root) {
|
|
|
1323
1324
|
* bun 无扩展名(bun.exe 可被 libuv 按 PATHEXT 解析),保持 'bun'。
|
|
1324
1325
|
*/
|
|
1325
1326
|
const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
1326
|
-
const MSM_ACTIONS = [
|
|
1327
|
-
"list",
|
|
1328
|
-
"exec",
|
|
1329
|
-
"register",
|
|
1330
|
-
"deregister",
|
|
1331
|
-
"check",
|
|
1332
|
-
"guide",
|
|
1333
|
-
"ccc-config",
|
|
1334
|
-
"catalog"
|
|
1335
|
-
];
|
|
1336
1327
|
/**
|
|
1337
1328
|
* ACC 能力目录(需求④ S142 用户拍板:"acc 配置各自做 guide 越来越多,整合成目录式使用指南")。
|
|
1338
1329
|
* 目录在前、详情各归各——**单一真相源**:本目录只索引"去哪个工具/子命令看详情",不复制详情
|
|
1339
|
-
* (避免
|
|
1330
|
+
* (避免 container_admin/weixin-doctor 等 guide 全文重复 → 多真相源 → 失同步)。
|
|
1340
1331
|
* 后续新增功能只需在此加一行(低熵可演进)。
|
|
1341
1332
|
*/
|
|
1342
1333
|
const ACC_CATALOG = `═══ ACC Usage Catalog ═══
|
|
1343
1334
|
Directory of ACC capabilities — each area lists where to go for details (guides live with their feature; this catalog only points).
|
|
1344
1335
|
|
|
1345
|
-
① 会话与轨迹 →
|
|
1346
|
-
session_rebuild(超限重建;阈值 K 见 ccc-config / 设置面板重建阈值K)
|
|
1347
|
-
② 认知质量框架 → eap
|
|
1348
|
-
③ 工具与执行 →
|
|
1349
|
-
acc_msm
|
|
1350
|
-
handyman(杂工 agent 编排——guide 子命令:
|
|
1351
|
-
④ 角色与对外面 →
|
|
1336
|
+
① 会话与轨迹 → logbook(list/show/create/use/close/health/qa/archive/summary/hook-develop-guide + rebuild 超限重建)
|
|
1337
|
+
session_rebuild 已并入 logbook rebuild(超限重建;阈值 K 见 container_admin ccc-config / 设置面板重建阈值K)
|
|
1338
|
+
② 认知质量框架 → praxis(eap/neat/cce 三合一——可实践理论注入,section 渐进披露,无参即目录)
|
|
1339
|
+
③ 工具与执行 → container_fs(容器文件系统 15 子命令)/ container_git(git)/ dashboard(health 含注册表检查/time/wait)
|
|
1340
|
+
acc_msm 已并入 msm(执行+发现)与 container_admin(管理)——msm("<name>", ["<args>"]);inspect=true 查用法
|
|
1341
|
+
handyman(杂工 agent 编排——guide 子命令:handyman guide)
|
|
1342
|
+
④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
|
|
1352
1343
|
ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
|
|
1353
1344
|
⑤ 自主与接入 → autopilot-trajectory(Autopilot 一站式:all/init/random/diag/doc/check/status/guide)
|
|
1354
|
-
weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:
|
|
1355
|
-
⑥ CCC 配置总览 →
|
|
1356
|
-
⑦ 注册表与安全 → mech-registry.json 由
|
|
1357
|
-
|
|
1345
|
+
weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
|
|
1346
|
+
⑥ CCC 配置总览 → container_admin msm ccc-config(8 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/autopilotTrajectory/weixin)
|
|
1347
|
+
⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
|
|
1348
|
+
dashboard health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
|
|
1358
1349
|
|
|
1359
1350
|
每区:一句话定位 + 详细入口(工具名 / guide 子命令)。详情永远以对应工具的 guide/ccc-config 为单一真相源。
|
|
1360
1351
|
`;
|
|
@@ -1364,9 +1355,9 @@ const MSM_GUIDE = `MSM Development Manual (Mech & Semi-Mech framework)
|
|
|
1364
1355
|
MSM = the executable-unit layer. Mech is pure TS with zero LLM reasoning; Semi-Mech is a TS framework + LLM decision points.
|
|
1365
1356
|
MSM is ACC's deterministic executable-unit layer — all shell/exec operations go through MSM and cannot be bypassed.
|
|
1366
1357
|
|
|
1367
|
-
## Registering a new MSM (
|
|
1358
|
+
## Registering a new MSM (container_admin msm register)
|
|
1368
1359
|
1. Write the script under <skill>/scripts/ (runnable via tsx; must include a main() CLI guard with an import.meta.url check)
|
|
1369
|
-
2.
|
|
1360
|
+
2. container_admin msm register --domain msm --action register <name> --skill <s> --path <script path relative to root> --category <mech|semi-mech> --description <desc>
|
|
1370
1361
|
3. Auto-writes mech-registry.json (preserving the original format) + git commit (commits only the registry file)
|
|
1371
1362
|
4. Validation: path must be inside root, script must exist, name must be globally unique
|
|
1372
1363
|
|
|
@@ -1377,7 +1368,7 @@ flags is a new-style object array for parameter validation and path-escape guard
|
|
|
1377
1368
|
{"name":"force","type":"boolean","description":"force mode","default":false}]
|
|
1378
1369
|
- {name, type} format — new style, type:"path" enables the path-escape guard
|
|
1379
1370
|
- {flag, description} format — old style, CLI flag description string
|
|
1380
|
-
- On registration, flags are passed via
|
|
1371
|
+
- On registration, flags are passed via container_admin msm register --flags '<json>'
|
|
1381
1372
|
|
|
1382
1373
|
## Script conventions
|
|
1383
1374
|
- Top-of-file documentation: purpose / usage / exit codes
|
|
@@ -1402,14 +1393,14 @@ flags is a new-style object array for parameter validation and path-escape guard
|
|
|
1402
1393
|
- Anti-example (forbidden): readline waiting for user input, process.stdin.on('data') blocking waits
|
|
1403
1394
|
(MSMs have no stdin interaction channel — they hang until the 600s timeout)
|
|
1404
1395
|
|
|
1405
|
-
## Quality checks (
|
|
1396
|
+
## Quality checks (container_admin msm check, DC-M1~M4)
|
|
1406
1397
|
DC-M1 has .test.ts/.spec.ts; DC-M2 has a main() guard (function main( / isMain / require.main === / import.meta.url);
|
|
1407
1398
|
DC-M3 bidirectional: scripts unregistered + registry references missing scripts; DC-M4 path-type flags marked type:"path"
|
|
1408
1399
|
|
|
1409
1400
|
## Self-description (protocol flags, first argument only)
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1401
|
+
msm("<name>", ["--list"]) — list all MSMs
|
|
1402
|
+
msm("<name>", ["--schema", "<n>"]) — view a specific MSM's parameter schema
|
|
1403
|
+
msm("<name>", ["--format=json"]) — JSON output mode (remaining args passed through losslessly)
|
|
1413
1404
|
`;
|
|
1414
1405
|
/** CCC 配置参考(对齐 osp ccc-config action) */
|
|
1415
1406
|
const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
|
|
@@ -1439,7 +1430,7 @@ trajectory-assistant(TRAJECTORY-ASSISTANT · CHECKPOINT)提醒机制的积
|
|
|
1439
1430
|
|
|
1440
1431
|
── 3. localstore.gitTrack ──
|
|
1441
1432
|
localstore.json 的 git 策略:allow 可提交 / deny 禁提交(默认 deny)。
|
|
1442
|
-
deny 时写入自动确保 .gitignore 含该文件(物理保证),
|
|
1433
|
+
deny 时写入自动确保 .gitignore 含该文件(物理保证),container_git commit 会检查拒绝。
|
|
1443
1434
|
|
|
1444
1435
|
Config:
|
|
1445
1436
|
{ "localstore": { "gitTrack": "allow" } }
|
|
@@ -1461,7 +1452,7 @@ Skiff 角色 = 全知全能 trajectory 的任意子集(CCC 定义)。每角
|
|
|
1461
1452
|
(MSM 白名单 msms[] 与非 MSM 工具白名单 tools[],白名单外全隐藏;skill 加载恒可用)。
|
|
1462
1453
|
trajectory 纪律子集(session/keeper/rebuild 参与项)默认全关 = 完全独立。
|
|
1463
1454
|
systemPrompt 内联 或 systemPromptFile(.md 引用,推荐)——角色会话的完整人格/边界提示词。
|
|
1464
|
-
validate 校验 / apply 生效 / list 查看:
|
|
1455
|
+
validate 校验 / apply 生效 / list 查看:container_admin role <guide|validate|apply|list>(skiff_admin 已并入)。
|
|
1465
1456
|
|
|
1466
1457
|
Config:
|
|
1467
1458
|
{ "skiff": { "roles": {
|
|
@@ -1477,7 +1468,7 @@ CCC 定义的一条自主 trajectory——时钟到点自动唤起(前台注
|
|
|
1477
1468
|
未配置或 enabled=false → 完全不启动(零资源占用)。多 CCC 独立:每 CCC 自己的配置。
|
|
1478
1469
|
唤起消息四段式:轨迹焦点 topPrompt(最先注入,稳定锚)→ 身份锚定 → 先验偏见
|
|
1479
1470
|
(CCC 根脚本 biasProvider 输出)→ 任务。目标会话 session 必填(目录须带 --auto 后缀)。
|
|
1480
|
-
诊断/状态/立即唤起:
|
|
1471
|
+
诊断/状态/立即唤起:autopilot-trajectory <all|check|status|diag>(msm("autopilot-trajectory", ["all"]))。
|
|
1481
1472
|
|
|
1482
1473
|
Config:
|
|
1483
1474
|
{ "autopilotTrajectory": {
|
|
@@ -1494,8 +1485,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
|
|
|
1494
1485
|
账号/路由/开关在此文件;**bot_token 凭据在 CCC localstore credential scope**
|
|
1495
1486
|
(扫码绑定后自动写入,永不进 git 明文面)。
|
|
1496
1487
|
路由 user → role:exact 优先,* 通配兜底;role 必须 ∈ 该 CCC skiff.roles。
|
|
1497
|
-
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;
|
|
1498
|
-
<status|diag|verify> 排查。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1488
|
+
面板(WebUI 设置 → 微信桥)可扫码绑定/移除账号/编辑路由;msm("weixin-doctor", ["status"|"diag"|"verify"]) 排查。凭据主动查看:localstore get WEIXIN_<ACCOUNT>_TOKEN。
|
|
1499
1489
|
|
|
1500
1490
|
Config:
|
|
1501
1491
|
{ "weixin": {
|
|
@@ -1669,7 +1659,7 @@ function runMsm(root, args) {
|
|
|
1669
1659
|
skill,
|
|
1670
1660
|
category,
|
|
1671
1661
|
description,
|
|
1672
|
-
usage: args.usage ?? `
|
|
1662
|
+
usage: args.usage ?? `msm ${name} [args...]`,
|
|
1673
1663
|
flags: flags ?? []
|
|
1674
1664
|
});
|
|
1675
1665
|
writeRegistry(regPath, entries, isV1Wrapped);
|
|
@@ -1870,7 +1860,7 @@ function msmExecResult(name, status, stdout, stderr, fmtJson, hasHelp = false) {
|
|
|
1870
1860
|
};
|
|
1871
1861
|
}
|
|
1872
1862
|
/**
|
|
1873
|
-
* 异步执行 MSM(
|
|
1863
|
+
* 异步执行 MSM(msm 工具主路径):
|
|
1874
1864
|
* 用 execFile + promisify + timeout(超时自动 kill),**不阻塞 Node 事件循环**。
|
|
1875
1865
|
* (同步 spawnSync 版会阻塞 web 事件循环 → MSM 脚本自请求 3080 时死锁,见 postmortem。)
|
|
1876
1866
|
*/
|
|
@@ -1930,9 +1920,23 @@ async function runMsmAsync(root, args) {
|
|
|
1930
1920
|
//#endregion
|
|
1931
1921
|
//#region src/tools/msm.ts
|
|
1932
1922
|
/**
|
|
1933
|
-
* msm.ts —
|
|
1923
|
+
* msm.ts — msm 工具(v1.30 重构:单入口执行,list/exec 合一)
|
|
1924
|
+
*
|
|
1925
|
+
* 设计(S142 用户拍板"msm 单入口同意 + 参数简易高效"):
|
|
1926
|
+
* acc_msm(8 action enum)→ msm(3 参数:name + args + inspect)——
|
|
1927
|
+
* 最高频工具参数极简:无 action 选择、无协议 flag 前置学习。
|
|
1928
|
+
*
|
|
1929
|
+
* LLM 直觉用法:
|
|
1930
|
+
* msm("ssh-connect", ["status"]) — 执行已注册 MSM
|
|
1931
|
+
* msm("ssh-connect") — 无参执行
|
|
1932
|
+
* msm("ssh-connect", [], inspect=true) — 查该 MSM 用法/参数(不执行)
|
|
1933
|
+
* msm("ssh") — 未精确命中 → 返回模糊候选(LLM 下轮选对)
|
|
1934
|
+
* msm() — 返回精简分类目录(替代旧全量 list)
|
|
1935
|
+
*
|
|
1936
|
+
* 管理面(register/deregister/check/guide/catalog/ccc-config)已移入 container_admin。
|
|
1937
|
+
* 底层 runMsmAsync/prepareExec 全复用;未命中候选为新增 findSuggestions。
|
|
1934
1938
|
*/
|
|
1935
|
-
function agentCwd$
|
|
1939
|
+
function agentCwd$6(exec) {
|
|
1936
1940
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1937
1941
|
}
|
|
1938
1942
|
function agentSessionId$1(exec) {
|
|
@@ -1944,48 +1948,46 @@ function renderText$9(value) {
|
|
|
1944
1948
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
1945
1949
|
}];
|
|
1946
1950
|
}
|
|
1951
|
+
/** 精简目录(无参调用返回;替代全量 list 的巨输出)——按 skill 分组统计 + 顶部指引 */
|
|
1952
|
+
function buildMsmIndex(root) {
|
|
1953
|
+
const entries = loadMsmEntries(root);
|
|
1954
|
+
if (entries.length === 0) return "(no MSM registered — call container_admin register to add one)";
|
|
1955
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
1956
|
+
for (const e of entries) {
|
|
1957
|
+
const k = e.skill ?? "uncategorized";
|
|
1958
|
+
bySkill.set(k, (bySkill.get(k) ?? 0) + 1);
|
|
1959
|
+
}
|
|
1960
|
+
const groups = [...bySkill.entries()].sort((a, b) => b[1] - a[1]).map(([skill, count]) => ` ${skill}: ${count} MSM`).join("\n");
|
|
1961
|
+
return `${entries.length} MSMs registered (by skill):\n${groups}\n\nExecute: msm("<name>", ["<args>"]) — e.g. msm("ssh-connect", ["status"])\nInspect: msm("<name>", [], inspect=true) — view usage/flags\nPartial name returns matching candidates.\nFull listing: use container_admin (register/deregister/check).`;
|
|
1962
|
+
}
|
|
1963
|
+
/** 模糊候选:name/description 子串匹配(未精确命中时返回 top-K,供 LLM 下轮选对) */
|
|
1964
|
+
function suggestMsm(root, query, limit = 5) {
|
|
1965
|
+
const q = query.toLowerCase();
|
|
1966
|
+
const hits = loadMsmEntries(root).filter((e) => e.name.toLowerCase().includes(q) || (e.description ?? "").toLowerCase().includes(q)).slice(0, limit);
|
|
1967
|
+
if (hits.length === 0) return `No MSM matches "${query}". Registered: ${buildMsmIndex(root)}`;
|
|
1968
|
+
const lines = hits.map((e) => {
|
|
1969
|
+
const base = `${e.name} | ${e.skill ?? "-"} | ${e.category ?? "-"} | ${e.description ?? ""}`;
|
|
1970
|
+
const flags = (e.flags ?? []).map((f) => `--${f.name} <${f.type ?? "string"}>`).join(", ");
|
|
1971
|
+
return flags ? `${base} [flags: ${flags}]` : base;
|
|
1972
|
+
});
|
|
1973
|
+
return `No exact MSM "${query}" — did you mean one of these?\n` + lines.join("\n");
|
|
1974
|
+
}
|
|
1947
1975
|
const msmTool = defineTool({
|
|
1948
|
-
name: "
|
|
1949
|
-
description: "MSM (Mech & Semi-Mech)
|
|
1976
|
+
name: "msm",
|
|
1977
|
+
description: "Execute a registered CCC MSM (Mech & Semi-Mech). msm(name, args) runs it directly; name not found returns matching candidates; inspect=true shows usage/flags without running; no args returns a summary index. The full CCC MSM list is discoverable via partial-name matching or the index.",
|
|
1950
1978
|
parameters: {
|
|
1951
|
-
action: {
|
|
1952
|
-
type: "string",
|
|
1953
|
-
enum: [...MSM_ACTIONS],
|
|
1954
|
-
required: true,
|
|
1955
|
-
description: "Subcommand: list/exec/register/deregister/check/guide/catalog/ccc-config"
|
|
1956
|
-
},
|
|
1957
1979
|
name: {
|
|
1958
1980
|
type: "string",
|
|
1959
|
-
description: "MSM name (
|
|
1981
|
+
description: "MSM name to execute (or a partial fragment — not found returns matching candidates)"
|
|
1960
1982
|
},
|
|
1961
1983
|
args: {
|
|
1962
1984
|
type: "array",
|
|
1963
1985
|
items: { type: "string" },
|
|
1964
|
-
description: "
|
|
1965
|
-
},
|
|
1966
|
-
skill: {
|
|
1967
|
-
type: "string",
|
|
1968
|
-
description: "register owning skill"
|
|
1969
|
-
},
|
|
1970
|
-
path: {
|
|
1971
|
-
type: "string",
|
|
1972
|
-
description: "register script relative path (must be inside root and exist)"
|
|
1973
|
-
},
|
|
1974
|
-
category: {
|
|
1975
|
-
type: "string",
|
|
1976
|
-
description: "register category (mech/semi-mech)"
|
|
1986
|
+
description: "Arguments passed to the MSM (optional; defaults to none). Pass \"--help\" as first arg to see an MSM's own usage."
|
|
1977
1987
|
},
|
|
1978
|
-
|
|
1979
|
-
type: "
|
|
1980
|
-
description: "
|
|
1981
|
-
},
|
|
1982
|
-
flags: {
|
|
1983
|
-
type: "string",
|
|
1984
|
-
description: "register flags JSON string (new-style object array; type:\"path\" enables escape validation)"
|
|
1985
|
-
},
|
|
1986
|
-
usage: {
|
|
1987
|
-
type: "string",
|
|
1988
|
-
description: "register custom usage (auto-generated by default)"
|
|
1988
|
+
inspect: {
|
|
1989
|
+
type: "boolean",
|
|
1990
|
+
description: "true = print the MSM's usage/flags/params WITHOUT executing (default false)"
|
|
1989
1991
|
}
|
|
1990
1992
|
},
|
|
1991
1993
|
output: {
|
|
@@ -1993,22 +1995,29 @@ const msmTool = defineTool({
|
|
|
1993
1995
|
render: (args, value) => renderText$9(value)
|
|
1994
1996
|
},
|
|
1995
1997
|
async execute(args, exec) {
|
|
1996
|
-
const root = findSerenityRoot(agentCwd$
|
|
1998
|
+
const root = findSerenityRoot(agentCwd$6(exec));
|
|
1997
1999
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1998
2000
|
const sessionId = agentSessionId$1(exec);
|
|
1999
|
-
const
|
|
2001
|
+
const name = args.name;
|
|
2002
|
+
const gate = skiffMsmGate(root, sessionId, name ? "exec" : "list", name);
|
|
2000
2003
|
if (gate.reject) throw new Error(gate.reject);
|
|
2001
|
-
if (
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2004
|
+
if (!name || name.trim() === "") return gate.whitelist ? "MSMs allowed in this role:\n" + [...gate.whitelist].join("\n") : buildMsmIndex(root);
|
|
2005
|
+
const clean = name.trim();
|
|
2006
|
+
if (gate.whitelist && !gate.whitelist.has(clean)) return `MSM "${clean}" is not in this role's whitelist. Allowed: ${[...gate.whitelist].join(", ")}`;
|
|
2007
|
+
if (!findEntry(root, clean)) {
|
|
2008
|
+
const suggest = suggestMsm(root, clean);
|
|
2009
|
+
return gate.whitelist ? `(whitelisted role — candidates filtered)\n${suggest}` : suggest;
|
|
2010
|
+
}
|
|
2011
|
+
if (args.inspect === true) return runMsmAsync(root, {
|
|
2012
|
+
action: "exec",
|
|
2013
|
+
name: clean,
|
|
2014
|
+
args: ["--schema", clean]
|
|
2015
|
+
});
|
|
2016
|
+
return runMsmAsync(root, {
|
|
2017
|
+
action: "exec",
|
|
2018
|
+
name: clean,
|
|
2019
|
+
args: args.args ?? []
|
|
2020
|
+
});
|
|
2012
2021
|
}
|
|
2013
2022
|
});
|
|
2014
2023
|
//#endregion
|
|
@@ -2050,7 +2059,7 @@ function renderText$8(value) {
|
|
|
2050
2059
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2051
2060
|
}];
|
|
2052
2061
|
}
|
|
2053
|
-
|
|
2062
|
+
defineTool({
|
|
2054
2063
|
name: "eap",
|
|
2055
2064
|
description: "EAP cognitive quality framework (progressive disclosure): defines E↑ explicitness / R↓ reconstructability / S↑ stability + pre-output self-check checklist. No section returns the full framework; specify a section to focus.",
|
|
2056
2065
|
parameters: { section: {
|
|
@@ -2111,7 +2120,7 @@ function renderText$7(value) {
|
|
|
2111
2120
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2112
2121
|
}];
|
|
2113
2122
|
}
|
|
2114
|
-
|
|
2123
|
+
defineTool({
|
|
2115
2124
|
name: "neat",
|
|
2116
2125
|
description: "Neat design collaboration protocol (progressive disclosure): small-step alignment / explicit decisions / document-driven / no level-skipping (requirements→scope→solution→interface→implementation). No section returns the full protocol.",
|
|
2117
2126
|
parameters: { section: {
|
|
@@ -2190,7 +2199,7 @@ function renderText$6(value) {
|
|
|
2190
2199
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2191
2200
|
}];
|
|
2192
2201
|
}
|
|
2193
|
-
|
|
2202
|
+
defineTool({
|
|
2194
2203
|
name: "cce",
|
|
2195
2204
|
description: "CCE cognitive continuity engineering (progressive disclosure): the engineering discipline of maintaining a cognitive entity's identity/accessibility/evolution under bounded resources and irreversible uncertainty. No section returns the full framework; specify a section to focus.",
|
|
2196
2205
|
parameters: { section: {
|
|
@@ -2232,6 +2241,55 @@ const cceTool = defineTool({
|
|
|
2232
2241
|
}
|
|
2233
2242
|
});
|
|
2234
2243
|
//#endregion
|
|
2244
|
+
//#region src/tools/praxis.ts
|
|
2245
|
+
/**
|
|
2246
|
+
* praxis.ts — praxis(可实践理论注入工具,v1.30 合并 eap/neat/cce)
|
|
2247
|
+
*
|
|
2248
|
+
* eap / neat / cce 三个知识工具合并为一个——它们都是「可实践理论」:
|
|
2249
|
+
* 调用时把对应框架注入当前认知(指导输出质量 / 协作协议 / 连续性工程)。
|
|
2250
|
+
* 名字:praxis(理论与实践的统一——理论注入实践,实践反哺理论)。
|
|
2251
|
+
* Metaphor 映射:Engineering Drawings(图纸 = 可实践理论的编码,第 3 条隐喻)。
|
|
2252
|
+
*
|
|
2253
|
+
* section 参数渐进披露:不传 = 返回精简目录;传 = 返回对应框架全文/章节。
|
|
2254
|
+
*/
|
|
2255
|
+
/** 知识源:与各原工具内容保持一致(单一真相源——eap/neat/cce 各自源文件已并入此处) */
|
|
2256
|
+
/** 框架清单(无参调用时返回的精简目录) */
|
|
2257
|
+
const PRAXIS_INDEX = `praxis — 可实践理论注入(认知框架,按需加载)
|
|
2258
|
+
选择框架:praxis eap(认知质量——E↑/R↓/S↑ 三变量 + 输出前自检)
|
|
2259
|
+
praxis neat(Neat 协作协议——小步对齐/显式决策/文档驱动)
|
|
2260
|
+
praxis cce(认知连续性工程——容器 5 行为约束 + H_op 操作熵)
|
|
2261
|
+
无参返回本目录;详情以对应框架原文为准(单一真相源)。
|
|
2262
|
+
`;
|
|
2263
|
+
function renderText$5(value) {
|
|
2264
|
+
return [{
|
|
2265
|
+
type: "text",
|
|
2266
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2267
|
+
}];
|
|
2268
|
+
}
|
|
2269
|
+
const praxisTool = defineTool({
|
|
2270
|
+
name: "praxis",
|
|
2271
|
+
description: "Praxis — actionable theory injection (cognitive frameworks, loaded on demand): eap (cognitive quality: E↑ explicitness / R↓ reconstructability / S↑ stability + self-check checklist) / neat (Neat design collaboration protocol: small-step alignment, explicit decisions, document-driven) / cce (Cognitive Continuity Engineering: container identity/accessibility/evolution under bounded resources). No section returns the index; pass a section to load that framework.",
|
|
2272
|
+
parameters: { section: {
|
|
2273
|
+
type: "string",
|
|
2274
|
+
enum: [
|
|
2275
|
+
"eap",
|
|
2276
|
+
"neat",
|
|
2277
|
+
"cce"
|
|
2278
|
+
],
|
|
2279
|
+
description: "Framework to inject: eap / neat / cce. Omit for the praxis index."
|
|
2280
|
+
} },
|
|
2281
|
+
output: {
|
|
2282
|
+
schema: { type: "string" },
|
|
2283
|
+
render: (_args, value) => renderText$5(value)
|
|
2284
|
+
},
|
|
2285
|
+
async execute(args) {
|
|
2286
|
+
if (args.section === "eap") return EAP_CONTENT;
|
|
2287
|
+
if (args.section === "neat") return NEAT_CONTENT;
|
|
2288
|
+
if (args.section === "cce") return CCE_CONTENT;
|
|
2289
|
+
return PRAXIS_INDEX;
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
//#endregion
|
|
2235
2293
|
//#region src/handyman-preset-inherit.ts
|
|
2236
2294
|
/**
|
|
2237
2295
|
* 解析 handyman worker 从父 agent 继承的 preset,并组装创建 setup 钩子。
|
|
@@ -2276,10 +2334,10 @@ function handymanPresetInheritance(parentCtx) {
|
|
|
2276
2334
|
* preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
|
|
2277
2335
|
* subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
|
|
2278
2336
|
*/
|
|
2279
|
-
function agentCwd$
|
|
2337
|
+
function agentCwd$5(exec) {
|
|
2280
2338
|
return (exec.agent?.session)?.header?.cwd ?? process.cwd();
|
|
2281
2339
|
}
|
|
2282
|
-
function renderText$
|
|
2340
|
+
function renderText$4(value) {
|
|
2283
2341
|
return [{
|
|
2284
2342
|
type: "text",
|
|
2285
2343
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2497,11 +2555,11 @@ function createHandymanTool(ctx) {
|
|
|
2497
2555
|
},
|
|
2498
2556
|
output: {
|
|
2499
2557
|
schema: { type: "json" },
|
|
2500
|
-
render: (_args, value) => renderText$
|
|
2558
|
+
render: (_args, value) => renderText$4(value)
|
|
2501
2559
|
},
|
|
2502
2560
|
async execute(args, exec) {
|
|
2503
2561
|
if (args.guide) return { guide: HANDYMAN_GUIDE };
|
|
2504
|
-
const root = findSerenityRoot(agentCwd$
|
|
2562
|
+
const root = findSerenityRoot(agentCwd$5(exec));
|
|
2505
2563
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2506
2564
|
const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
|
|
2507
2565
|
if (hc === null) throw new Error("handyman requires a model whitelist: configure .opencode/serenity.json \"handyman.models\" (e.g. {\"handyman\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
|
|
@@ -2545,727 +2603,6 @@ function createHandymanTool(ctx) {
|
|
|
2545
2603
|
});
|
|
2546
2604
|
}
|
|
2547
2605
|
//#endregion
|
|
2548
|
-
//#region src/session-ops.ts
|
|
2549
|
-
/**
|
|
2550
|
-
* session-ops.ts — session 工具纯操作层(零 DSH 依赖,可独立单测)
|
|
2551
|
-
*
|
|
2552
|
-
* 行为对齐 osp(opencode-serenity-plugin/src/session/lib.ts)——osp 是 ACC 工具 spec:
|
|
2553
|
-
* - create:--desc / --issue 二选一(互斥、缺省报错);issue 模式目录 YYYY-MM-DD--<issue>
|
|
2554
|
-
* (无 S###,sessionId=issue);desc 模式 YYYY-MM-DD--S###--<desc>;goal 写入目标段;dry-run 预览
|
|
2555
|
-
* - close:需 name + confirm=true;标记 [x] 已完成+已关闭 + 进度记录"关闭"
|
|
2556
|
-
* - archive:name 缺省 → 批量归档(completed + ≥7 天 → 移动 _archived/);单会话需 completed + grace
|
|
2557
|
-
* - list/show/health/qa/summary:文本输出格式与 osp 一致
|
|
2558
|
-
* 保留 dsp S134 活跃会话机制(内存 Map + events 恢复,不落盘)——osp 同为内存 active-state。
|
|
2559
|
-
*/
|
|
2560
|
-
const SESSION_ACTIONS = [
|
|
2561
|
-
"list",
|
|
2562
|
-
"show",
|
|
2563
|
-
"create",
|
|
2564
|
-
"use",
|
|
2565
|
-
"close",
|
|
2566
|
-
"health",
|
|
2567
|
-
"qa",
|
|
2568
|
-
"archive",
|
|
2569
|
-
"summary",
|
|
2570
|
-
"hook-develop-guide"
|
|
2571
|
-
];
|
|
2572
|
-
/**
|
|
2573
|
-
* 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
|
|
2574
|
-
* 移除 `.events` 属性 → `snapshotEvents()` 方法(dsh-session/src/session.ts:
|
|
2575
|
-
* `snapshotEvents(fromSeq, toSeqExclusive)`)。插件早期代码多处裸读 `.events`
|
|
2576
|
-
* (经 `as unknown as { events? }` 断言绕过 typecheck),运行时静默 undefined——
|
|
2577
|
-
* 造成 first-anchor 每轮重插 / SESSION 激活恢复失效 / rebuild 定位错乱。
|
|
2578
|
-
* 统一收敛到本 helper:snapshotEvents() 优先(rc.1 真实形态),`.events` 兜底
|
|
2579
|
-
* (测试替身/旧运行时)。所有消费方一律经此读取,禁止再裸读 `.events`。
|
|
2580
|
-
* 泛型 T:调用方按需声明事件形状(如 `SessionEvent`),unknown 默认。
|
|
2581
|
-
*/
|
|
2582
|
-
function sessionEvents(session) {
|
|
2583
|
-
const s = session;
|
|
2584
|
-
if (!s) return [];
|
|
2585
|
-
if (typeof s.snapshotEvents === "function") try {
|
|
2586
|
-
return s.snapshotEvents() ?? [];
|
|
2587
|
-
} catch {}
|
|
2588
|
-
return s.events ?? [];
|
|
2589
|
-
}
|
|
2590
|
-
const SESSION_MD = "SESSION.md";
|
|
2591
|
-
const ARCHIVE_DIR_NAME = "_archived";
|
|
2592
|
-
const HEALTH_STALE_DAYS = 7;
|
|
2593
|
-
const HEALTH_STALLED_PCT = 30;
|
|
2594
|
-
const HEALTH_STALLED_DAYS = 3;
|
|
2595
|
-
const HEALTH_GHOST_DAYS = 2;
|
|
2596
|
-
const DAY = 864e5;
|
|
2597
|
-
function sessionsRoot(root) {
|
|
2598
|
-
return join(root, "AGENT_SESSIONS");
|
|
2599
|
-
}
|
|
2600
|
-
/** 解析 SESSION.md 状态元数据(对齐 osp parseSessionMd) */
|
|
2601
|
-
function parseSessionMd(filePath) {
|
|
2602
|
-
try {
|
|
2603
|
-
const content = readFileSync(filePath, "utf-8");
|
|
2604
|
-
return {
|
|
2605
|
-
hasSessionMd: true,
|
|
2606
|
-
completed: /\[\s*x\s*\]/i.test(content),
|
|
2607
|
-
completedCount: (content.match(/\[\s*x\s*\]/gi) ?? []).length,
|
|
2608
|
-
pendingCount: (content.match(/\[\s*[ \t]\s*\]/g) ?? []).length,
|
|
2609
|
-
unresolvedCount: (content.match(/(未解决|open|question|TODO)/gi) ?? []).length
|
|
2610
|
-
};
|
|
2611
|
-
} catch {
|
|
2612
|
-
return {
|
|
2613
|
-
hasSessionMd: false,
|
|
2614
|
-
completed: false,
|
|
2615
|
-
completedCount: 0,
|
|
2616
|
-
pendingCount: 0,
|
|
2617
|
-
unresolvedCount: 0
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
function readSessionEntry(dirPath) {
|
|
2622
|
-
try {
|
|
2623
|
-
const st = statSync(dirPath);
|
|
2624
|
-
if (!st.isDirectory()) return null;
|
|
2625
|
-
const dirName = basename(dirPath);
|
|
2626
|
-
const mdPath = join(dirPath, SESSION_MD);
|
|
2627
|
-
const status = existsSync(mdPath) ? parseSessionMd(mdPath) : {
|
|
2628
|
-
hasSessionMd: false,
|
|
2629
|
-
completed: false,
|
|
2630
|
-
completedCount: 0,
|
|
2631
|
-
pendingCount: 0,
|
|
2632
|
-
unresolvedCount: 0
|
|
2633
|
-
};
|
|
2634
|
-
return {
|
|
2635
|
-
dirName,
|
|
2636
|
-
path: dirPath,
|
|
2637
|
-
mtime: st.mtime,
|
|
2638
|
-
status
|
|
2639
|
-
};
|
|
2640
|
-
} catch {
|
|
2641
|
-
return null;
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
/** 读取 AGENT_SESSIONS 中所有会话,活跃(未完成)排前(对齐 osp readAllSessions) */
|
|
2645
|
-
function readAllSessions(sessionsDir) {
|
|
2646
|
-
try {
|
|
2647
|
-
return readdirSync(sessionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => readSessionEntry(join(sessionsDir, e.name))).filter((s) => s !== null).sort((a, b) => {
|
|
2648
|
-
if (!a.status.completed && b.status.completed) return -1;
|
|
2649
|
-
if (a.status.completed && !b.status.completed) return 1;
|
|
2650
|
-
return b.mtime.getTime() - a.mtime.getTime();
|
|
2651
|
-
});
|
|
2652
|
-
} catch {
|
|
2653
|
-
return [];
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
/** 提取目录名中的会话 ID(S### 或 issue 名);无匹配返回 '' */
|
|
2657
|
-
function extractSessionId(dirName) {
|
|
2658
|
-
const m = dirName.match(/--S(\d{3,})--/);
|
|
2659
|
-
return m ? `S${m[1]}` : "";
|
|
2660
|
-
}
|
|
2661
|
-
/**
|
|
2662
|
-
* 根据 key 查找会话(对齐 osp findSession):
|
|
2663
|
-
* 精确目录名 → S### ID(允许 S31→031)→ 唯一模糊子串匹配(多个则报错)
|
|
2664
|
-
*/
|
|
2665
|
-
function findSession(sessionsDir, key) {
|
|
2666
|
-
const all = readAllSessions(sessionsDir);
|
|
2667
|
-
const byName = all.find((s) => s.dirName === key);
|
|
2668
|
-
if (byName) return byName;
|
|
2669
|
-
const searchId = key.replace(/^S/, "").padStart(3, "0");
|
|
2670
|
-
const byId = all.find((s) => {
|
|
2671
|
-
const m = s.dirName.match(/--S(\d{3,})--/);
|
|
2672
|
-
return m && m[1] === searchId;
|
|
2673
|
-
});
|
|
2674
|
-
if (byId) return byId;
|
|
2675
|
-
const lower = key.toLowerCase();
|
|
2676
|
-
const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(lower));
|
|
2677
|
-
if (fuzzy.length === 1) return fuzzy[0] ?? null;
|
|
2678
|
-
if (fuzzy.length > 1) throw new Error(`Found ${fuzzy.length} sessions matching "${key}": ` + fuzzy.map((s) => s.dirName).join(", ") + ". Use a more specific query.");
|
|
2679
|
-
return null;
|
|
2680
|
-
}
|
|
2681
|
-
/** list 子命令(对齐 osp listSessions 文本格式 + active 标记) */
|
|
2682
|
-
function listSessions(root, activeId) {
|
|
2683
|
-
const sessions = readAllSessions(sessionsRoot(root));
|
|
2684
|
-
if (sessions.length === 0) return "(no sessions in AGENT_SESSIONS/)";
|
|
2685
|
-
const lines = sessions.map((s) => {
|
|
2686
|
-
const age = Math.floor((Date.now() - s.mtime.getTime()) / DAY);
|
|
2687
|
-
const sessionId = extractSessionId(s.dirName);
|
|
2688
|
-
return `${activeId !== void 0 && sessionId !== "" && sessionId === activeId ? "●" : s.status.completed ? "✓" : "○"} ${s.dirName} (${age}d ago)`;
|
|
2689
|
-
});
|
|
2690
|
-
return `AGENT_SESSIONS/ (${sessions.length} sessions)\n` + lines.join("\n");
|
|
2691
|
-
}
|
|
2692
|
-
/** show 子命令(对齐 osp showSession:`# dirName\n\n` + SESSION.md 内容) */
|
|
2693
|
-
function showSession(root, key) {
|
|
2694
|
-
const session = findSession(sessionsRoot(root), key);
|
|
2695
|
-
if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
|
|
2696
|
-
const mdPath = join(session.path, SESSION_MD);
|
|
2697
|
-
if (!existsSync(mdPath)) return `Session ${session.dirName} (no SESSION.md — directory exists but is empty)`;
|
|
2698
|
-
const content = readFileSync(mdPath, "utf-8");
|
|
2699
|
-
return `# ${session.dirName}\n\n${content}`;
|
|
2700
|
-
}
|
|
2701
|
-
/** 生成 SESSION.md 模板(对齐 osp:goal 写入目标段,时间戳 YYYY-MM-DD HH:mm) */
|
|
2702
|
-
function sessionMdTemplate(title, id, goal, now) {
|
|
2703
|
-
const ts = now.toISOString().slice(0, 16).replace("T", " ");
|
|
2704
|
-
return `# SESSION: ${title}\n- ID: ${id}\n\n## 目标\n${goal ?? "(待补充)"}\n\n## 状态\n- [ ] 进行中\n\n## 关键决策\n| # | 决策 | 理由 |\n|---|------|------|\n| 1 | | |\n\n## 进度记录\n- ${ts} — 创建\n\n## 产出物\n- \n\n## 未解决的问题\n- \n`;
|
|
2705
|
-
}
|
|
2706
|
-
/** create 子命令(对齐 osp createSession:--desc/--issue 二选一 + dry-run + 长度限制) */
|
|
2707
|
-
/** 目录名脱敏(Windows 审计问题 10):非法字符 → '-', 去尾点/空格, 保留名(CON/NUL 等)加前缀 */
|
|
2708
|
-
function sanitizeDirName(s) {
|
|
2709
|
-
const cleaned = s.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "");
|
|
2710
|
-
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(cleaned)) return `_${cleaned}`;
|
|
2711
|
-
return cleaned;
|
|
2712
|
-
}
|
|
2713
|
-
function createSession(opts) {
|
|
2714
|
-
const { root, desc, issue, goal, dryRun } = opts;
|
|
2715
|
-
const sessionsDir = sessionsRoot(root);
|
|
2716
|
-
const now = /* @__PURE__ */ new Date();
|
|
2717
|
-
const datePrefix = now.toISOString().slice(0, 10);
|
|
2718
|
-
if (!desc && !issue) throw new Error("create requires either --desc or --issue");
|
|
2719
|
-
if (desc && issue) throw new Error("--desc and --issue are mutually exclusive");
|
|
2720
|
-
if (issue) {
|
|
2721
|
-
if (issue.length > 100) throw new Error(`issue too long: ${issue.length} chars (max 100)`);
|
|
2722
|
-
const dirName = `${datePrefix}--${sanitizeDirName(issue)}`;
|
|
2723
|
-
const sessionPath = join(sessionsDir, dirName);
|
|
2724
|
-
if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
|
|
2725
|
-
if (dryRun) return {
|
|
2726
|
-
message: `[dry-run] Would create: ${dirName}/`,
|
|
2727
|
-
dirName,
|
|
2728
|
-
sessionPath,
|
|
2729
|
-
sessionId: issue
|
|
2730
|
-
};
|
|
2731
|
-
mkdirSync(sessionPath, { recursive: true });
|
|
2732
|
-
writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(issue, issue, goal, now), "utf-8");
|
|
2733
|
-
return {
|
|
2734
|
-
message: `Created: ${dirName}/`,
|
|
2735
|
-
dirName,
|
|
2736
|
-
sessionPath,
|
|
2737
|
-
sessionId: issue
|
|
2738
|
-
};
|
|
2739
|
-
}
|
|
2740
|
-
if (!desc || desc.length === 0) throw new Error("description cannot be empty");
|
|
2741
|
-
if (desc.length > 200) throw new Error(`description too long: ${desc.length} chars (max 200)`);
|
|
2742
|
-
const sessions = readAllSessions(sessionsDir);
|
|
2743
|
-
let maxId = 0;
|
|
2744
|
-
for (const s of sessions) {
|
|
2745
|
-
const m = s.dirName.match(/--S(\d{3,})--/);
|
|
2746
|
-
if (m) {
|
|
2747
|
-
const num = parseInt(m[1], 10);
|
|
2748
|
-
if (num > maxId) maxId = num;
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
const nextId = String(maxId + 1).padStart(3, "0");
|
|
2752
|
-
const dirName = `${datePrefix}--S${nextId}--${sanitizeDirName(desc)}`;
|
|
2753
|
-
const sessionPath = join(sessionsDir, dirName);
|
|
2754
|
-
if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
|
|
2755
|
-
if (dryRun) return {
|
|
2756
|
-
message: `[dry-run] Would create: ${dirName}/\n goal=${goal ?? "(none)"}`,
|
|
2757
|
-
dirName,
|
|
2758
|
-
sessionPath,
|
|
2759
|
-
sessionId: `S${nextId}`
|
|
2760
|
-
};
|
|
2761
|
-
mkdirSync(sessionPath, { recursive: true });
|
|
2762
|
-
writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(desc, `S${nextId}`, goal, now), "utf-8");
|
|
2763
|
-
return {
|
|
2764
|
-
message: `Created: ${dirName}/ (S${nextId})`,
|
|
2765
|
-
dirName,
|
|
2766
|
-
sessionPath,
|
|
2767
|
-
sessionId: `S${nextId}`
|
|
2768
|
-
};
|
|
2769
|
-
}
|
|
2770
|
-
/**
|
|
2771
|
-
* 活动会话跟踪(S134 v1.16.14 内存化,对齐 osp active-state):
|
|
2772
|
-
* **不落盘**——活跃会话状态在内存 Map(key = scope = dsh 会话 id),避免落盘标记
|
|
2773
|
-
* 文件累积与跨会话串台(落盘版 `.dsh/active-sessions/<scope>` 已被此方案取代)。
|
|
2774
|
-
* 进程重启恢复:从**当前会话历史(events)**解析 `[SESSION CONTEXT]` 标记(use 时注入),
|
|
2775
|
-
* 只扫自己会话——无全局扫描、无跨会话污染。
|
|
2776
|
-
*/
|
|
2777
|
-
const DEFAULT_SESSION_SCOPE = "default";
|
|
2778
|
-
/** [SESSION CONTEXT] 恢复标记(use 时注入 events 历史;进程重启后从 events 解析) */
|
|
2779
|
-
const SESSION_CONTEXT_MARKER = "[SESSION CONTEXT] Activated:";
|
|
2780
|
-
/** 内存活跃会话:scope(dsh 会话 id)→ 会话信息(不落盘;并行多会话各自 key 隔离) */
|
|
2781
|
-
const activeStore = /* @__PURE__ */ new Map();
|
|
2782
|
-
/** 全局最近活跃(对齐 osp lastActive;供无 scope 上下文使用) */
|
|
2783
|
-
let lastActive = null;
|
|
2784
|
-
function getActiveSessionInfo(scope) {
|
|
2785
|
-
return activeStore.get(scope) ?? null;
|
|
2786
|
-
}
|
|
2787
|
-
function setActiveSessionInfo(scope, info) {
|
|
2788
|
-
activeStore.set(scope, info);
|
|
2789
|
-
lastActive = info;
|
|
2790
|
-
}
|
|
2791
|
-
function clearActiveSessionInfo(scope) {
|
|
2792
|
-
activeStore.delete(scope);
|
|
2793
|
-
if (lastActive && ![...activeStore.values()].some((v) => v === lastActive)) lastActive = null;
|
|
2794
|
-
}
|
|
2795
|
-
/** 当前 scope 的活跃会话 SESSION.md 绝对路径;无激活返回 null(读内存,不落盘) */
|
|
2796
|
-
function readActiveSessionMd(_root, scope = DEFAULT_SESSION_SCOPE) {
|
|
2797
|
-
return getActiveSessionInfo(scope)?.mdPath ?? null;
|
|
2798
|
-
}
|
|
2799
|
-
/**
|
|
2800
|
-
* use 子命令:激活会话(写内存 Map)+ 返回对齐 osp 的输出文本
|
|
2801
|
-
* (含 [SESSION CONTEXT] 标记 + todowrite 指令;标记随工具结果进 events 历史,
|
|
2802
|
-
* 进程重启后从当前会话 events 解析恢复)。
|
|
2803
|
-
*/
|
|
2804
|
-
function useSession(root, key, scope = DEFAULT_SESSION_SCOPE) {
|
|
2805
|
-
const session = findSession(sessionsRoot(root), key);
|
|
2806
|
-
if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
|
|
2807
|
-
const mdPath = join(session.path, SESSION_MD);
|
|
2808
|
-
if (!existsSync(mdPath)) throw new Error(`Session "${session.dirName}" has no SESSION.md — nothing to load.`);
|
|
2809
|
-
const sessionId = extractSessionId(session.dirName) || basename(session.dirName);
|
|
2810
|
-
const dirName = session.dirName;
|
|
2811
|
-
const shortName = dirName.replace(/^\d{4}-\d{2}-\d{2}--/, "");
|
|
2812
|
-
setActiveSessionInfo(scope, {
|
|
2813
|
-
sessionId,
|
|
2814
|
-
dirName,
|
|
2815
|
-
mdPath
|
|
2816
|
-
});
|
|
2817
|
-
return {
|
|
2818
|
-
dir: dirName,
|
|
2819
|
-
mdPath,
|
|
2820
|
-
context: [
|
|
2821
|
-
`───────────────────────────────────────────────────────────────`,
|
|
2822
|
-
`${SESSION_CONTEXT_MARKER} ${dirName}`,
|
|
2823
|
-
`───────────────────────────────────────────────────────────────`,
|
|
2824
|
-
`Use "session show ${sessionId}" to view session details.`,
|
|
2825
|
-
`SESSION.md path: ${mdPath}`,
|
|
2826
|
-
``,
|
|
2827
|
-
`→ All subsequent work should refer back to this session.`,
|
|
2828
|
-
` Use "session show ${sessionId}" to check current progress.`,
|
|
2829
|
-
` After advancing work, update the "进度记录" (progress) section in SESSION.md.`,
|
|
2830
|
-
``,
|
|
2831
|
-
`→ BEFORE responding to the user, you MUST call todowrite immediately`,
|
|
2832
|
-
` with the session todo list. The first item MUST be:`,
|
|
2833
|
-
` content: "SESSION: ${sessionId} — ${shortName}"`,
|
|
2834
|
-
` status: "completed", priority: "low"`,
|
|
2835
|
-
` Follow with any tasks parsed from SESSION.md.`,
|
|
2836
|
-
`───────────────────────────────────────────────────────────────`
|
|
2837
|
-
].join("\n")
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
/**
|
|
2841
|
-
* close 子命令(对齐 osp closeSession):需 name + confirm=true;
|
|
2842
|
-
* 标记 SESSION.md 为 [x] 已完成 + [x] 已关闭 + 进度记录"关闭";清除该会话的活跃状态。
|
|
2843
|
-
*/
|
|
2844
|
-
function closeSession(root, key, confirm, scope = DEFAULT_SESSION_SCOPE) {
|
|
2845
|
-
if (!confirm) return "⚠ Close requires explicit confirmation.\n Re-run with --confirm to confirm closing this session.";
|
|
2846
|
-
const session = findSession(sessionsRoot(root), key);
|
|
2847
|
-
if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
|
|
2848
|
-
if (session.status.completed) return `Session "${session.dirName}" is already completed.`;
|
|
2849
|
-
const mdPath = join(session.path, SESSION_MD);
|
|
2850
|
-
if (!existsSync(mdPath)) throw new Error(`Session "${session.dirName}" has no SESSION.md — nothing to close.`);
|
|
2851
|
-
let content = readFileSync(mdPath, "utf-8");
|
|
2852
|
-
content = content.replace(/\r\n/g, "\n");
|
|
2853
|
-
content = content.replace(/## 状态\n\n?- \[ \] 进行中/, "## 状态\n- [x] 已完成\n- [x] 已关闭");
|
|
2854
|
-
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
2855
|
-
if (!content.includes("-- 关闭")) content = content.replace(/(## 进度记录\n)/, `$1- ${now} — 关闭\n`);
|
|
2856
|
-
writeFileSync(mdPath, content, "utf-8");
|
|
2857
|
-
clearActiveSessionInfo(scope);
|
|
2858
|
-
return `Session "${session.dirName}" closed and marked as completed.`;
|
|
2859
|
-
}
|
|
2860
|
-
/**
|
|
2861
|
-
* 从会话历史(events)解析会话身份(进程重启恢复;只扫**当前会话**自己的历史——无跨会话串台)。
|
|
2862
|
-
*
|
|
2863
|
-
* v1.24.11 稳固化(S142 用户需求:重建后新会话必须准确知道从哪个 SESSION 恢复):
|
|
2864
|
-
* **路径规范行即可,不再要求 [SESSION CONTEXT] 标记**——`session use` 上下文与重建锚点
|
|
2865
|
-
* (buildRebuildAnchor 的 `- Persistent trajectory — SESSION.md path: <rel>` 行)同格式,
|
|
2866
|
-
* 因此**仅靠重建锚点的会话(从未显式 use)同样可恢复**。从**尾到头**扫描,最后一条
|
|
2867
|
-
* 合法路径胜出(时间序最新);会话目录名/ID 从路径本身派生(单一真相源,不猜)。
|
|
2868
|
-
* 路径可为相对(重建锚点存 rel)——绝对化与存在性校验在调用方(知道 root)执行。
|
|
2869
|
-
*/
|
|
2870
|
-
function parseSessionContextFromEvents(events) {
|
|
2871
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
2872
|
-
const strs = [];
|
|
2873
|
-
collectStrings(events[i], strs);
|
|
2874
|
-
for (const s of strs) {
|
|
2875
|
-
const md = extractSessionMdPathFromText(s);
|
|
2876
|
-
if (!md) continue;
|
|
2877
|
-
let dirName;
|
|
2878
|
-
let filePath;
|
|
2879
|
-
if (basename(md) === SESSION_MD) {
|
|
2880
|
-
dirName = basename(dirname(md));
|
|
2881
|
-
filePath = md;
|
|
2882
|
-
} else if (isSessionDirName(basename(md))) {
|
|
2883
|
-
dirName = basename(md);
|
|
2884
|
-
filePath = join(md, SESSION_MD);
|
|
2885
|
-
} else continue;
|
|
2886
|
-
if (!isSessionDirName(dirName)) continue;
|
|
2887
|
-
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
2888
|
-
return {
|
|
2889
|
-
sessionId: idMatch ? `S${idMatch[1]}` : dirName,
|
|
2890
|
-
dirName,
|
|
2891
|
-
mdPath: filePath
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
return null;
|
|
2896
|
-
}
|
|
2897
|
-
/** 递归收集对象/数组/字符串中的全部字符串(保留原文,无 JSON 转义) */
|
|
2898
|
-
function collectStrings(v, out) {
|
|
2899
|
-
if (typeof v === "string") {
|
|
2900
|
-
out.push(v);
|
|
2901
|
-
return;
|
|
2902
|
-
}
|
|
2903
|
-
if (v && typeof v === "object") for (const val of Object.values(v)) collectStrings(val, out);
|
|
2904
|
-
}
|
|
2905
|
-
/** 规范行正则:`SESSION.md path: <路径>`(路径可含空格 → [^\r\n]+ 整行匹配) */
|
|
2906
|
-
const SESSION_MD_PATH_RE = /SESSION\.md path:\s*([^\r\n]+)/;
|
|
2907
|
-
/** 从文本提取 SESSION.md 路径(use 上下文 / 重建锚点规范行通用;无匹配返回 null)。
|
|
2908
|
-
* 同行已知尾注(如系统提示词 Session 块的 persistent-body 注释)剥除——规范行只取路径本体。 */
|
|
2909
|
-
function extractSessionMdPathFromText(text) {
|
|
2910
|
-
const m = text.match(SESSION_MD_PATH_RE);
|
|
2911
|
-
if (!m) return null;
|
|
2912
|
-
let p = m[1].trim();
|
|
2913
|
-
const suffix = p.search(/ \(the trajectory's persistent body/);
|
|
2914
|
-
if (suffix > 0) p = p.slice(0, suffix).trim();
|
|
2915
|
-
return p;
|
|
2916
|
-
}
|
|
2917
|
-
/** 会话目录名形态校验(createSession 恒带日期前缀 `YYYY-MM-DD--`) */
|
|
2918
|
-
function isSessionDirName(dirName) {
|
|
2919
|
-
return /^\d{4}-\d{2}-\d{2}--/.test(dirName) && dirName.length > 11;
|
|
2920
|
-
}
|
|
2921
|
-
/**
|
|
2922
|
-
* 从 dsh 会话标题解析 SESSION 目录(编码无关 best-match,U3/U4)——
|
|
2923
|
-
* 标题不假设 S### 前缀(CCC 可自定义编码:apaas-xxx / P### / 完整目录名等)。
|
|
2924
|
-
* 匹配优先级(全部对 AGENT_SESSIONS 现有目录 best-match,不猜):
|
|
2925
|
-
* ① 标题即完整目录名(含日期前缀 `YYYY-MM-DD--`)→ 精确命中
|
|
2926
|
-
* ② 标题含 `--<code>--` 段(如完整目录名被截断为 `<code>-日期-概括` 前段)→
|
|
2927
|
-
* 按 code 段匹配:取标题首 token(`-` 前),与各目录 `--<code>--`/`--<code>` 尾段比对
|
|
2928
|
-
* ③ 唯一模糊子串匹配(多个则返回 null 防误猜)
|
|
2929
|
-
* @param title dsh 会话标题(如 `S142-2026-08-24-概括` / `apaas-26116-…` / 完整目录名)
|
|
2930
|
-
* @param sessionsDir AGENT_SESSIONS 绝对路径
|
|
2931
|
-
* @returns 命中目录的绝对路径(SESSION.md);无/歧义返回 null
|
|
2932
|
-
*/
|
|
2933
|
-
function resolveSessionByTitle(title, sessionsDir) {
|
|
2934
|
-
const t = (title ?? "").trim();
|
|
2935
|
-
if (!t) return null;
|
|
2936
|
-
const all = readAllSessions(sessionsDir);
|
|
2937
|
-
if (all.length === 0) return null;
|
|
2938
|
-
if (isSessionDirName(t)) {
|
|
2939
|
-
const exact = all.find((s) => s.dirName === t);
|
|
2940
|
-
if (exact) return join(exact.path, SESSION_MD);
|
|
2941
|
-
}
|
|
2942
|
-
const codeToken = t.split("-")[0]?.trim() ?? "";
|
|
2943
|
-
if (codeToken) {
|
|
2944
|
-
const byCode = all.filter((s) => {
|
|
2945
|
-
const m = s.dirName.match(/--([^--]+)--/);
|
|
2946
|
-
const code = m ? m[1] : null;
|
|
2947
|
-
const tailMatch = s.dirName.match(/--([^--]+)$/);
|
|
2948
|
-
const tailCode = tailMatch && !s.dirName.includes("--", s.dirName.lastIndexOf("--") + 3) ? tailMatch[1] : null;
|
|
2949
|
-
return code === codeToken || tailCode === codeToken || s.dirName === codeToken || s.dirName.includes(`--${codeToken}`);
|
|
2950
|
-
});
|
|
2951
|
-
if (byCode.length === 1) return join(byCode[0].path, SESSION_MD);
|
|
2952
|
-
if (byCode.length > 1) return null;
|
|
2953
|
-
}
|
|
2954
|
-
const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(t.toLowerCase()));
|
|
2955
|
-
if (fuzzy.length === 1) return join(fuzzy[0].path, SESSION_MD);
|
|
2956
|
-
return null;
|
|
2957
|
-
}
|
|
2958
|
-
/**
|
|
2959
|
-
* 约定回退(v1.24.11):AGENT_SESSIONS 下最新修改的**未完成**会话的 SESSION.md。
|
|
2960
|
-
* readAllSessions 已按「未完成优先 + mtime 降序」排序 → 首个未完成且含 SESSION.md 即最新活动。
|
|
2961
|
-
* 只作最后手段(内存/events/锚点全缺时),保证重建锚点至少指向一个真实存在的轨迹。
|
|
2962
|
-
*/
|
|
2963
|
-
function findLatestActiveSessionMd(root) {
|
|
2964
|
-
for (const s of readAllSessions(sessionsRoot(root))) {
|
|
2965
|
-
if (s.status.completed) continue;
|
|
2966
|
-
const md = join(s.path, SESSION_MD);
|
|
2967
|
-
if (existsSync(md)) return md;
|
|
2968
|
-
}
|
|
2969
|
-
return null;
|
|
2970
|
-
}
|
|
2971
|
-
/** health 子命令(对齐 osp healthCheck:stale/stalled/ghost/drift 四类检查,文本输出) */
|
|
2972
|
-
function healthCheck(root) {
|
|
2973
|
-
const sessions = readAllSessions(sessionsRoot(root));
|
|
2974
|
-
if (sessions.length === 0) return "No sessions found — nothing to check.";
|
|
2975
|
-
const now = Date.now();
|
|
2976
|
-
const issues = [];
|
|
2977
|
-
for (const s of sessions) {
|
|
2978
|
-
const ageDays = (now - s.mtime.getTime()) / DAY;
|
|
2979
|
-
const st = s.status;
|
|
2980
|
-
if (ageDays > HEALTH_STALE_DAYS && !st.completed) issues.push({
|
|
2981
|
-
dirName: s.dirName,
|
|
2982
|
-
issue: `No activity for ${Math.floor(ageDays)}d`,
|
|
2983
|
-
severity: "stale"
|
|
2984
|
-
});
|
|
2985
|
-
const totalTasks = st.completedCount + st.pendingCount;
|
|
2986
|
-
if (totalTasks > 0) {
|
|
2987
|
-
const pct = Math.round(st.completedCount / totalTasks * 100);
|
|
2988
|
-
if (pct < HEALTH_STALLED_PCT && ageDays > HEALTH_STALLED_DAYS && !st.completed) issues.push({
|
|
2989
|
-
dirName: s.dirName,
|
|
2990
|
-
issue: `Only ${pct}% done after ${Math.floor(ageDays)}d`,
|
|
2991
|
-
severity: "stalled"
|
|
2992
|
-
});
|
|
2993
|
-
}
|
|
2994
|
-
if (!st.hasSessionMd && ageDays > HEALTH_GHOST_DAYS) issues.push({
|
|
2995
|
-
dirName: s.dirName,
|
|
2996
|
-
issue: "No SESSION.md (ghost directory)",
|
|
2997
|
-
severity: "ghost"
|
|
2998
|
-
});
|
|
2999
|
-
if (st.unresolvedCount > 3 && !st.completed) issues.push({
|
|
3000
|
-
dirName: s.dirName,
|
|
3001
|
-
issue: `${st.unresolvedCount} unresolved items`,
|
|
3002
|
-
severity: "drift"
|
|
3003
|
-
});
|
|
3004
|
-
}
|
|
3005
|
-
if (issues.length === 0) return "All sessions healthy — no issues found.";
|
|
3006
|
-
const lines = issues.map((i) => `[${i.severity.toUpperCase()}] ${i.dirName}: ${i.issue}`);
|
|
3007
|
-
return `${issues.length} issue(s) found:\n` + lines.join("\n");
|
|
3008
|
-
}
|
|
3009
|
-
/** archive 子命令(对齐 osp archiveSessions:移动 _archived/;name 缺省批量) */
|
|
3010
|
-
function archiveSessions(root, opts) {
|
|
3011
|
-
const { name, dryRun } = opts;
|
|
3012
|
-
const sessionsDir = sessionsRoot(root);
|
|
3013
|
-
const now = Date.now();
|
|
3014
|
-
const archiveDir = join(sessionsDir, ARCHIVE_DIR_NAME);
|
|
3015
|
-
if (name) {
|
|
3016
|
-
const session = findSession(sessionsDir, name);
|
|
3017
|
-
if (!session) throw new Error(`Session not found: "${name}"`);
|
|
3018
|
-
if (!session.status.completed) return `Session "${session.dirName}" is not completed — skipping.`;
|
|
3019
|
-
const ageDays = (now - session.mtime.getTime()) / DAY;
|
|
3020
|
-
if (ageDays < 7) return `Session "${session.dirName}" completed ${Math.floor(ageDays)}d ago — needs ${7 - Math.floor(ageDays)} more days before archiving.`;
|
|
3021
|
-
if (dryRun) return `[dry-run] Would archive: ${session.dirName} → ${ARCHIVE_DIR_NAME}/`;
|
|
3022
|
-
if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
|
|
3023
|
-
renameSync(session.path, join(archiveDir, session.dirName));
|
|
3024
|
-
return `Archived: ${session.dirName} → _archived/`;
|
|
3025
|
-
}
|
|
3026
|
-
const toArchive = readAllSessions(sessionsDir).filter((s) => {
|
|
3027
|
-
if (!s.status.completed) return false;
|
|
3028
|
-
return (now - s.mtime.getTime()) / DAY >= 7;
|
|
3029
|
-
});
|
|
3030
|
-
if (toArchive.length === 0) return "No sessions eligible for archiving.";
|
|
3031
|
-
if (dryRun) return `[dry-run] Would archive ${toArchive.length} session(s):\n` + toArchive.map((s) => ` ${s.dirName}`).join("\n");
|
|
3032
|
-
if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
|
|
3033
|
-
let count = 0;
|
|
3034
|
-
for (const s of toArchive) {
|
|
3035
|
-
renameSync(s.path, join(archiveDir, s.dirName));
|
|
3036
|
-
count++;
|
|
3037
|
-
}
|
|
3038
|
-
return `Archived ${count} session(s) → _archived/`;
|
|
3039
|
-
}
|
|
3040
|
-
/** summary 子命令(对齐 osp sessionSummary 文本仪表盘) */
|
|
3041
|
-
function summarize(root) {
|
|
3042
|
-
const sessions = readAllSessions(sessionsRoot(root));
|
|
3043
|
-
if (sessions.length === 0) return "AGENT_SESSIONS/ is empty.";
|
|
3044
|
-
const now = Date.now();
|
|
3045
|
-
const completed = sessions.filter((s) => s.status.completed).length;
|
|
3046
|
-
const active = sessions.length - completed;
|
|
3047
|
-
const stale = sessions.filter((s) => !s.status.completed && (now - s.mtime.getTime()) / DAY > HEALTH_STALE_DAYS).length;
|
|
3048
|
-
const ghost = sessions.filter((s) => !s.status.hasSessionMd).length;
|
|
3049
|
-
const recent = sessions.slice(0, 5);
|
|
3050
|
-
const lines = [
|
|
3051
|
-
`AGENT_SESSIONS Summary`,
|
|
3052
|
-
`────────────────────────`,
|
|
3053
|
-
`Total: ${sessions.length}`,
|
|
3054
|
-
`Active: ${active}`,
|
|
3055
|
-
`Completed: ${completed}`,
|
|
3056
|
-
`Stale: ${stale}`,
|
|
3057
|
-
`Ghost: ${ghost}`,
|
|
3058
|
-
``,
|
|
3059
|
-
`Recent activity (top 5):`,
|
|
3060
|
-
...recent.map((s) => {
|
|
3061
|
-
const age = Math.floor((now - s.mtime.getTime()) / DAY);
|
|
3062
|
-
return ` ${s.status.completed ? "✓" : "○"} ${s.dirName} (${age}d ago)`;
|
|
3063
|
-
})
|
|
3064
|
-
];
|
|
3065
|
-
if (stale > 0) lines.push("", "⚠ Warning: Stale sessions found — run \"session health\" for details.");
|
|
3066
|
-
return lines.join("\n");
|
|
3067
|
-
}
|
|
3068
|
-
/** 事实核对:SESSION.md 声明 vs 实际情况(结构/一致性/新鲜度/决策质量/产出物) */
|
|
3069
|
-
function qaCheck(root, key) {
|
|
3070
|
-
const session = findSession(sessionsRoot(root), key);
|
|
3071
|
-
if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
|
|
3072
|
-
const mdPath = join(session.path, SESSION_MD);
|
|
3073
|
-
if (!existsSync(mdPath)) return `[ERROR] Session "${session.dirName}" has no SESSION.md — nothing to verify.`;
|
|
3074
|
-
const content = readFileSync(mdPath, "utf-8");
|
|
3075
|
-
const issues = [];
|
|
3076
|
-
for (const section of [
|
|
3077
|
-
{
|
|
3078
|
-
heading: "目标",
|
|
3079
|
-
label: "目标 (goal)"
|
|
3080
|
-
},
|
|
3081
|
-
{
|
|
3082
|
-
heading: "状态",
|
|
3083
|
-
label: "状态 (status)"
|
|
3084
|
-
},
|
|
3085
|
-
{
|
|
3086
|
-
heading: "关键决策",
|
|
3087
|
-
label: "关键决策 (key decisions)"
|
|
3088
|
-
},
|
|
3089
|
-
{
|
|
3090
|
-
heading: "进度记录",
|
|
3091
|
-
label: "进度记录 (progress)"
|
|
3092
|
-
},
|
|
3093
|
-
{
|
|
3094
|
-
heading: "产出物",
|
|
3095
|
-
label: "产出物 (outputs)"
|
|
3096
|
-
},
|
|
3097
|
-
{
|
|
3098
|
-
heading: "未解决的问题",
|
|
3099
|
-
label: "未解决的问题 (unresolved)"
|
|
3100
|
-
}
|
|
3101
|
-
]) {
|
|
3102
|
-
const headingRegex = new RegExp(`^##\\s*${section.heading}[\\s\\S]*?(?=^##|(?![\\s\\S]))`, "m");
|
|
3103
|
-
const match = content.match(headingRegex);
|
|
3104
|
-
if (!match) {
|
|
3105
|
-
issues.push({
|
|
3106
|
-
severity: "warning",
|
|
3107
|
-
category: "structure",
|
|
3108
|
-
message: `Missing section: ${section.label}`
|
|
3109
|
-
});
|
|
3110
|
-
continue;
|
|
3111
|
-
}
|
|
3112
|
-
const headingLineRegex = new RegExp(`^##\\s*${section.heading}\\s*$`, "m");
|
|
3113
|
-
const body = match[0].replace(headingLineRegex, "").trim();
|
|
3114
|
-
if (!body || /^[-*]\s*$/.test(body)) issues.push({
|
|
3115
|
-
severity: "warning",
|
|
3116
|
-
category: "structure",
|
|
3117
|
-
message: `Section "${section.label}" is empty (only placeholder)`
|
|
3118
|
-
});
|
|
3119
|
-
}
|
|
3120
|
-
const completedTasks = (content.match(/\[\s*x\s*\]/gi) ?? []).length;
|
|
3121
|
-
const pendingTasks = (content.match(/\[\s*[ \t]\s*\]/gi) ?? []).length;
|
|
3122
|
-
const statusSection = content.match(/^##\s*状态[\s\S]*?(?=^##|(?![^]))/im);
|
|
3123
|
-
const statusBody = statusSection ? statusSection[0].replace(/^##\s*状态.*$/m, "").trim() : "";
|
|
3124
|
-
const hasCompletionMark = statusBody ? /#+\s*(?:完成|done|completed|closed)\b/i.test(statusBody) || /(?:全部完成|已全部完成|所有.*任务.*完成|任务.*全部完成|已完成.*所有)/i.test(statusBody) : false;
|
|
3125
|
-
const unresolvedSection = content.match(/^##\s*未解决的问题[\s\S]*?(?=^##|(?![^]))/im);
|
|
3126
|
-
const unresolvedBody = unresolvedSection ? unresolvedSection[0].replace(/^##\s*未解决的问题.*$/m, "").trim() : "";
|
|
3127
|
-
const unresolvedCount = unresolvedBody ? (unresolvedBody.match(/(?:未解决|open|question|TODO)/gi) ?? []).length : 0;
|
|
3128
|
-
if (hasCompletionMark && pendingTasks > 0) issues.push({
|
|
3129
|
-
severity: "error",
|
|
3130
|
-
category: "consistency",
|
|
3131
|
-
message: `Session marked as completed but has ${pendingTasks} pending task(s)`
|
|
3132
|
-
});
|
|
3133
|
-
if (hasCompletionMark && unresolvedCount > 0) issues.push({
|
|
3134
|
-
severity: "warning",
|
|
3135
|
-
category: "consistency",
|
|
3136
|
-
message: `Session marked as completed but has ${unresolvedCount} unresolved item(s)`
|
|
3137
|
-
});
|
|
3138
|
-
if (completedTasks > 0 && pendingTasks === 0 && !hasCompletionMark) issues.push({
|
|
3139
|
-
severity: "info",
|
|
3140
|
-
category: "consistency",
|
|
3141
|
-
message: `All ${completedTasks} task(s) completed but session not marked complete`
|
|
3142
|
-
});
|
|
3143
|
-
const progressSection = content.match(/##\s*进度记录[\s\S]*?(?=^##|\z)/m);
|
|
3144
|
-
if (progressSection) {
|
|
3145
|
-
const dateMatches = progressSection[0].match(/\b(\d{4}-\d{2}-\d{2})\b/g);
|
|
3146
|
-
if (dateMatches && dateMatches.length > 0) {
|
|
3147
|
-
const lastDateStr = dateMatches[dateMatches.length - 1];
|
|
3148
|
-
const lastDate = new Date(lastDateStr);
|
|
3149
|
-
const daysSince = Math.floor((Date.now() - lastDate.getTime()) / DAY);
|
|
3150
|
-
if (daysSince > HEALTH_STALE_DAYS && pendingTasks > 0) issues.push({
|
|
3151
|
-
severity: "warning",
|
|
3152
|
-
category: "stale",
|
|
3153
|
-
message: `No progress entry for ${daysSince} days (last: ${lastDateStr}), session still has ${pendingTasks} pending task(s)`
|
|
3154
|
-
});
|
|
3155
|
-
}
|
|
3156
|
-
}
|
|
3157
|
-
const decisionSection = content.match(/##\s*关键决策[\s\S]*?(?=^##|\z)/m);
|
|
3158
|
-
if (decisionSection) {
|
|
3159
|
-
const decisionLines = decisionSection[0].split("\n").filter((l) => /^\|\s*\d+\s*\|/.test(l));
|
|
3160
|
-
if (decisionLines.length > 0) {
|
|
3161
|
-
const emptyDecisions = decisionLines.filter((l) => {
|
|
3162
|
-
const cells = l.split("|").map((c) => c.trim());
|
|
3163
|
-
return cells.length >= 4 && (!cells[2] || !cells[3] || cells[2] === "-" || cells[3] === "-");
|
|
3164
|
-
});
|
|
3165
|
-
if (emptyDecisions.length > 0) issues.push({
|
|
3166
|
-
severity: "info",
|
|
3167
|
-
category: "quality",
|
|
3168
|
-
message: `${emptyDecisions.length} decision(s) have empty reason — consider filling gaps`
|
|
3169
|
-
});
|
|
3170
|
-
} else if (!hasCompletionMark) issues.push({
|
|
3171
|
-
severity: "info",
|
|
3172
|
-
category: "quality",
|
|
3173
|
-
message: "No decisions recorded yet — add key decisions as the session progresses"
|
|
3174
|
-
});
|
|
3175
|
-
}
|
|
3176
|
-
const outputSection = content.match(/##\s*产出物[\s\S]*?(?=^##|\z)/m);
|
|
3177
|
-
if (outputSection) {
|
|
3178
|
-
const outputLines = outputSection[0].split("\n").filter((l) => /^\s*[-*]\s/.test(l));
|
|
3179
|
-
const fileRefs = [];
|
|
3180
|
-
for (const line of outputLines) {
|
|
3181
|
-
const refs = line.match(/`[^`]+`/g) ?? [];
|
|
3182
|
-
fileRefs.push(...refs.map((r) => r.replace(/`/g, "")));
|
|
3183
|
-
const inlineRefs = line.match(/\b[\w./-]+\.[a-zA-Z]{1,5}\b/g) ?? [];
|
|
3184
|
-
fileRefs.push(...inlineRefs.filter((r) => r.includes("/") || r.includes(".")));
|
|
3185
|
-
}
|
|
3186
|
-
if (fileRefs.length > 0) {
|
|
3187
|
-
const missing = fileRefs.filter((ref) => !existsSync(join(root, ref)));
|
|
3188
|
-
if (missing.length > 0 && completedTasks > 0) issues.push({
|
|
3189
|
-
severity: "warning",
|
|
3190
|
-
category: "outputs",
|
|
3191
|
-
message: `${missing.length} referenced file(s) not found: ${missing.slice(0, 3).join(", ")}${missing.length > 3 ? `... (+${missing.length - 3} more)` : ""}`
|
|
3192
|
-
});
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
3196
|
-
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
3197
|
-
const infoCount = issues.filter((i) => i.severity === "info").length;
|
|
3198
|
-
const verified = errorCount === 0 && warningCount === 0;
|
|
3199
|
-
const lines = [
|
|
3200
|
-
`QA Report: ${session.dirName}`,
|
|
3201
|
-
`────────────────${"─".repeat(session.dirName.length)}`,
|
|
3202
|
-
`Summary: ${issues.length} issue(s) found (${errorCount} error, ${warningCount} warning, ${infoCount} info)`,
|
|
3203
|
-
`Status: ${verified ? "✓ Verified" : "⚠ Issues found"}`
|
|
3204
|
-
];
|
|
3205
|
-
if (issues.length > 0) {
|
|
3206
|
-
lines.push("");
|
|
3207
|
-
for (const issue of issues) {
|
|
3208
|
-
const tag = issue.severity === "error" ? "ERR" : issue.severity === "warning" ? "WRN" : "INF";
|
|
3209
|
-
lines.push(` [${tag}:${issue.category}] ${issue.message}`);
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
lines.push("", "Recommendations:");
|
|
3213
|
-
if (errorCount > 0) lines.push(" • Fix errors before closing the session (status vs content mismatch)");
|
|
3214
|
-
if (warningCount > 0) lines.push(" • Review warnings — they may indicate incomplete or outdated information");
|
|
3215
|
-
if (verified) lines.push(" • Session looks clean — no issues detected");
|
|
3216
|
-
return lines.join("\n");
|
|
3217
|
-
}
|
|
3218
|
-
//#endregion
|
|
3219
|
-
//#region src/session-bound.ts
|
|
3220
|
-
/** 事件 type 常量(声明 + 读取共用) */
|
|
3221
|
-
const SESSION_BOUND_EVENT = "serenity/bound";
|
|
3222
|
-
/** 事件形状归一(运行时数据经 session.append 深冻结,此处仅类型收窄) */
|
|
3223
|
-
function asBoundEvent(e) {
|
|
3224
|
-
const ev = e;
|
|
3225
|
-
if (!ev || ev.type !== "serenity/bound") return null;
|
|
3226
|
-
const d = ev.data;
|
|
3227
|
-
if (!d || typeof d.dirName !== "string" || typeof d.mdPath !== "string") return null;
|
|
3228
|
-
return d;
|
|
3229
|
-
}
|
|
3230
|
-
/**
|
|
3231
|
-
* 读取会话日志中**最后一条** `serenity/bound`(权威绑定,latest-wins)。
|
|
3232
|
-
* 尾到头扫描(时间序最新在后);无 bound 事件返回 null。
|
|
3233
|
-
* 纯逻辑零 DSH 依赖(经 sessionEvents helper 读 snapshotEvents/events 兜底)。
|
|
3234
|
-
*/
|
|
3235
|
-
function readLastBound(session) {
|
|
3236
|
-
const events = sessionEvents(session);
|
|
3237
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
3238
|
-
const b = asBoundEvent(events[i]);
|
|
3239
|
-
if (b) return b;
|
|
3240
|
-
}
|
|
3241
|
-
return null;
|
|
3242
|
-
}
|
|
3243
|
-
/**
|
|
3244
|
-
* append 一条 `serenity/bound` 绑定事件(log-only,无 surfaceOp——元数据不进模型可见面)。
|
|
3245
|
-
* @param session 目标 dsh 会话(真实 Session;append 泛型经内部断言兼容——类型由
|
|
3246
|
-
* `serenity/bound` 声明面保证,append 接受已声明事件)
|
|
3247
|
-
* @param action 绑定动作
|
|
3248
|
-
* @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
|
|
3249
|
-
* @returns 是否成功(append 抛错/会话不可用时 false——绑定失败不阻断主流程)
|
|
3250
|
-
*/
|
|
3251
|
-
function appendBound(session, action, rec) {
|
|
3252
|
-
if (!session || typeof session.append !== "function") return false;
|
|
3253
|
-
try {
|
|
3254
|
-
const append = session.append;
|
|
3255
|
-
append(SESSION_BOUND_EVENT, {
|
|
3256
|
-
dirName: rec.dirName,
|
|
3257
|
-
mdPath: rec.mdPath,
|
|
3258
|
-
...rec.sessionId ? { sessionId: rec.sessionId } : {},
|
|
3259
|
-
action,
|
|
3260
|
-
at: Date.now(),
|
|
3261
|
-
...rec.note ? { note: rec.note } : {}
|
|
3262
|
-
});
|
|
3263
|
-
return true;
|
|
3264
|
-
} catch {
|
|
3265
|
-
return false;
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
//#endregion
|
|
3269
2606
|
//#region src/tools/session.ts
|
|
3270
2607
|
/**
|
|
3271
2608
|
* session.ts — session 真实 DSH 工具定义(defineTool)
|
|
@@ -3283,7 +2620,7 @@ function appendBound(session, action, rec) {
|
|
|
3283
2620
|
* 同步把当前 dsh 会话重命名为该 SESSION 目录名**(sessionTitle.rename,user source
|
|
3284
2621
|
* pin 住标题)。非创建时预命名。
|
|
3285
2622
|
*/
|
|
3286
|
-
function agentCwd$
|
|
2623
|
+
function agentCwd$4(exec) {
|
|
3287
2624
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3288
2625
|
}
|
|
3289
2626
|
/** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
|
|
@@ -3399,7 +2736,7 @@ function renameDshSessionForActive(ctx, exec, info, summary) {
|
|
|
3399
2736
|
console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
|
|
3400
2737
|
}
|
|
3401
2738
|
}
|
|
3402
|
-
function renderText$
|
|
2739
|
+
function renderText$3(value) {
|
|
3403
2740
|
return [{
|
|
3404
2741
|
type: "text",
|
|
3405
2742
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -3424,10 +2761,10 @@ function discoverCccSubcommands(entries) {
|
|
|
3424
2761
|
}
|
|
3425
2762
|
/** 生成扩展提示(对齐 osp buildExtHint) */
|
|
3426
2763
|
function buildExtHint(hasSessionTool, hooks, subcommands) {
|
|
3427
|
-
if (!hasSessionTool) return "\n\n[CCC] To extend session capabilities, register a session-tool MSM (
|
|
2764
|
+
if (!hasSessionTool) return "\n\n[CCC] To extend session capabilities, register a session-tool MSM (container_admin msm register); see session hook-develop-guide";
|
|
3428
2765
|
const parts = [];
|
|
3429
2766
|
if (hooks.length > 0) parts.push(`hooks: ${hooks.join(", ")}`);
|
|
3430
|
-
if (subcommands.length > 0) parts.push(`custom subcommands (
|
|
2767
|
+
if (subcommands.length > 0) parts.push(`custom subcommands (msm session-tool): ${subcommands.join(", ")}`);
|
|
3431
2768
|
return `\n\n[CCC] session-tool MSM registered${parts.length > 0 ? ` (${parts.join("; ")})` : ""}`;
|
|
3432
2769
|
}
|
|
3433
2770
|
/** hook-develop-guide 内容(对齐 osp getHookDevelopGuide) */
|
|
@@ -3449,13 +2786,13 @@ function getHookDevelopGuide(hasSessionTool) {
|
|
|
3449
2786
|
"",
|
|
3450
2787
|
" create-transform",
|
|
3451
2788
|
" Trigger: after create writes the default SESSION.md",
|
|
3452
|
-
" Invocation:
|
|
2789
|
+
" Invocation: msm session-tool --hook=create-transform --session-dir=<path>",
|
|
3453
2790
|
" Allowed: read SESSION.md and modify it in place (append fields, swap templates, call APIs, etc.)",
|
|
3454
2791
|
" Note: ACC has already ensured the directory and SESSION.md exist; the CCC only modifies",
|
|
3455
2792
|
"",
|
|
3456
2793
|
"── Extension point 2: Custom subcommands ──",
|
|
3457
2794
|
"",
|
|
3458
|
-
"The LLM can call
|
|
2795
|
+
"The LLM can call msm session-tool <subcommand> to run CCC-specific",
|
|
3459
2796
|
"subcommands such as reindex, export, batch-create. These bypass the ACC session",
|
|
3460
2797
|
"tool's enum.",
|
|
3461
2798
|
"",
|
|
@@ -3465,7 +2802,7 @@ function getHookDevelopGuide(hasSessionTool) {
|
|
|
3465
2802
|
" .opencode/skills/<ccc-name>/scripts/session-tool.ts",
|
|
3466
2803
|
"",
|
|
3467
2804
|
"2. Register it in mech-registry.json:",
|
|
3468
|
-
"
|
|
2805
|
+
" container_admin msm register session-tool \\",
|
|
3469
2806
|
" --skill <ccc-name> --path .opencode/skills/<ccc-name>/scripts/session-tool.ts \\",
|
|
3470
2807
|
" --category semi-mech \\",
|
|
3471
2808
|
" --description \"CCC session extension: hooks + custom subcommands\" \\",
|
|
@@ -3482,9 +2819,9 @@ function getHookDevelopGuide(hasSessionTool) {
|
|
|
3482
2819
|
"",
|
|
3483
2820
|
"4. Subcommand declaration convention:",
|
|
3484
2821
|
" The --subcommand description field in flags enumerates supported subcommand names, split by |.",
|
|
3485
|
-
" The LLM can then call
|
|
2822
|
+
" The LLM can then call msm session-tool <subcommand>.",
|
|
3486
2823
|
"",
|
|
3487
|
-
hasSessionTool ? "✅ This CCC has a session-tool MSM registered" : "ℹ️ This CCC has no session-tool MSM yet — start with
|
|
2824
|
+
hasSessionTool ? "✅ This CCC has a session-tool MSM registered" : "ℹ️ This CCC has no session-tool MSM yet — start with container_admin msm register",
|
|
3488
2825
|
"",
|
|
3489
2826
|
"── More information ──",
|
|
3490
2827
|
"",
|
|
@@ -3497,19 +2834,23 @@ function getHookDevelopGuide(hasSessionTool) {
|
|
|
3497
2834
|
*/
|
|
3498
2835
|
function createSessionTool(ctx) {
|
|
3499
2836
|
return defineTool({
|
|
3500
|
-
name: "
|
|
3501
|
-
description: "
|
|
2837
|
+
name: "logbook",
|
|
2838
|
+
description: "The Logbook (AGENT_SESSIONS/ trajectory log — the voyage's persistent record). Full work-session lifecycle: list/show/create/use/close/health/qa/archive/summary/hook-develop-guide + rebuild (trajectory-tracker overflow: clear and rebuild the current conversation in place, Ship of Theseus). create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars, content summary — required, except --dry-run preview and --issue sessions which are exempt); close requires --confirm; use activates the session for the current dsh conversation (in-memory + events restore, isolated per dsh session) and requires --summary (≤20 chars). rebuild requires --summary (next-phase content summary ≤20 chars) + optional --note (task focus for the rebuilt self). The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the S### id and date stay server-derived.",
|
|
3502
2839
|
parameters: {
|
|
3503
2840
|
action: {
|
|
3504
2841
|
type: "string",
|
|
3505
2842
|
enum: [...SESSION_ACTIONS],
|
|
3506
2843
|
required: true,
|
|
3507
|
-
description: "Subcommand: list (status summary) / show (S### or dir name or fuzzy keyword) / create (--desc or --issue) / use (activate context, closed can be reopened) / close (requires --name + --confirm, irreversible) / health (stale/stalled/drift/ghost) / qa (fact check) / archive (archive, name defaults to batch) / summary (dashboard) / hook-develop-guide (CCC extension guide)"
|
|
2844
|
+
description: "Subcommand: list (status summary) / show (S### or dir name or fuzzy keyword) / create (--desc or --issue) / use (activate context, closed can be reopened) / close (requires --name + --confirm, irreversible) / health (stale/stalled/drift/ghost) / qa (fact check) / archive (archive, name defaults to batch) / summary (dashboard) / rebuild (clear-and-rebuild current conversation, Ship of Theseus — requires --summary + optional --note) / hook-develop-guide (CCC extension guide)"
|
|
3508
2845
|
},
|
|
3509
2846
|
name: {
|
|
3510
2847
|
type: "string",
|
|
3511
2848
|
description: "show/use/close/archive/qa session identifier (S### or dir name or keyword)"
|
|
3512
2849
|
},
|
|
2850
|
+
note: {
|
|
2851
|
+
type: "string",
|
|
2852
|
+
description: "rebuild: task focus ≤200 chars for the rebuilt self — what to work on next (short, no history; SESSION.md holds the full history). Injected as \"- Task focus: …\" into the rebuild anchor."
|
|
2853
|
+
},
|
|
3513
2854
|
desc: {
|
|
3514
2855
|
type: "string",
|
|
3515
2856
|
description: "create short description (any language, ≤5 words; mutually exclusive with issue)"
|
|
@@ -3541,10 +2882,10 @@ function createSessionTool(ctx) {
|
|
|
3541
2882
|
},
|
|
3542
2883
|
output: {
|
|
3543
2884
|
schema: { type: "json" },
|
|
3544
|
-
render: (args, value) => renderText$
|
|
2885
|
+
render: (args, value) => renderText$3(value)
|
|
3545
2886
|
},
|
|
3546
2887
|
async execute(args, exec) {
|
|
3547
|
-
const root = findSerenityRoot(agentCwd$
|
|
2888
|
+
const root = findSerenityRoot(agentCwd$4(exec));
|
|
3548
2889
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
3549
2890
|
const entries = loadMsmEntries(root);
|
|
3550
2891
|
const hasSessionTool = entries.some((e) => e.name === "session-tool");
|
|
@@ -3641,724 +2982,33 @@ function createSessionTool(ctx) {
|
|
|
3641
2982
|
case "qa":
|
|
3642
2983
|
if (!args.name) throw new Error("qa requires name (S### or directory name)");
|
|
3643
2984
|
return qaCheck(root, args.name) + extHint;
|
|
2985
|
+
case "rebuild": {
|
|
2986
|
+
if (!args.summary || args.summary.trim() === "") throw new Error("rebuild requires --summary <content summary ≤20 chars> (the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild)");
|
|
2987
|
+
const dshSessionId = exec.agent?.session?.id ?? "";
|
|
2988
|
+
if (!dshSessionId) throw new Error("Unable to determine the current dsh session id");
|
|
2989
|
+
const { queueRebuild } = await Promise.resolve().then(() => rebuild_exports);
|
|
2990
|
+
const result = await queueRebuild(ctx, {
|
|
2991
|
+
root,
|
|
2992
|
+
note: args.note,
|
|
2993
|
+
summary: args.summary,
|
|
2994
|
+
agentCwd: agentCwd$4(exec),
|
|
2995
|
+
dshSessionId
|
|
2996
|
+
});
|
|
2997
|
+
return {
|
|
2998
|
+
ok: true,
|
|
2999
|
+
queued: result.queued,
|
|
3000
|
+
anchor: result.anchor,
|
|
3001
|
+
sessionMdPath: result.sessionMdPath,
|
|
3002
|
+
instruction: "Queued clear-and-rebuild: when this turn ends, the same conversation will be cleared and injected with the \"continue the work\" anchor (first-anchor protocol body included), then auto-continue — no manual input needed; resume from SESSION.md at that point."
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3644
3005
|
default: throw new Error(`Unknown action: ${args.action}`);
|
|
3645
3006
|
}
|
|
3646
3007
|
}
|
|
3647
3008
|
});
|
|
3648
3009
|
}
|
|
3649
3010
|
//#endregion
|
|
3650
|
-
//#region src/
|
|
3651
|
-
/** 默认首请求工具集:dsp 平台 Minimal 等价核心(anchored 是 bash+str_replace_editor) */
|
|
3652
|
-
const DEFAULT_BOOTSTRAP_TOOLS = [
|
|
3653
|
-
"read",
|
|
3654
|
-
"write",
|
|
3655
|
-
"edit",
|
|
3656
|
-
"glob",
|
|
3657
|
-
"grep"
|
|
3658
|
-
];
|
|
3659
|
-
/** 默认剥离的自动注入源(anchored 默认:可用技能目录提醒 + 工作区指令摘要) */
|
|
3660
|
-
const DEFAULT_SUPPRESSED_SOURCES = ["skill-catalog", "agent-instructions"];
|
|
3661
|
-
/** 默认 compaction 恢复工作集(anchored 默认 read/write/edit/glob/grep/todo_write/ask_user_question) */
|
|
3662
|
-
const DEFAULT_COMPACTION_TOOLS = [
|
|
3663
|
-
"read",
|
|
3664
|
-
"write",
|
|
3665
|
-
"edit",
|
|
3666
|
-
"glob",
|
|
3667
|
-
"grep",
|
|
3668
|
-
"todo_write"
|
|
3669
|
-
];
|
|
3670
|
-
/**
|
|
3671
|
-
* 协议级锚定消息序列(用户确认固化,2026-08-24 S142):任何 CCC 在抽象层都是
|
|
3672
|
-
* 宁静号/ACC——首轮锚定话语统一,无 CCC 配置面。两轮递进:
|
|
3673
|
-
* ① ACC 身份 + EAP 原则 + we/us 人称 + 先锚定后行动
|
|
3674
|
-
* ② 协作协议 5 条 + acknowledge 要求(只回确认,不做事)
|
|
3675
|
-
* 文本 = 原 home-serenity CCC 配置(anchorMessages),随零配置化固化进代码。
|
|
3676
|
-
*/
|
|
3677
|
-
const DEFAULT_ANCHOR_MESSAGES = ["You are the operator agent of Serenity — a cognitive container governed by the Abstract Cognitive Container (ACC) protocol.\nWe operate under the Explicit Abstraction Principle (EAP): the functional value of a thought equals its external reconstructability — every output we produce must be explicit (E↑), reconstructable (R↓), and stable (S↑).\nThe personal pronoun is us/we.\nWe anchor first, then act: the abstract layer precedes the concrete.\nPlease simply reply \"acknowledge\" — no action needed.", "Before we proceed, align on how we work:\n1. We read before we write — every decision grounds in what already exists in the container.\n2. Every output records its reasoning (R↓): decisions carry reasons and alternatives.\n3. We never jump levels — abstract layer first, then specifics.\n4. Every artifact we create is a durable cognitive anchor for the work that follows.\n5. We keep the container's state coherent (SESSION.md) as we advance.\nPlease simply reply \"acknowledge\" — no action needed."];
|
|
3678
|
-
/**
|
|
3679
|
-
* 根会话锚定重入判定(v1.28.1 提取导出以便单测):会话是否已有真实用户消息。
|
|
3680
|
-
* resume/续跑的会话已有对话历史 → **不重锚**(first-anchor 只注入一次)。
|
|
3681
|
-
*
|
|
3682
|
-
* 0.1.2-rc.1 适配教训:裸读 `session.events`(经类型断言)恒 undefined →
|
|
3683
|
-
* `!undefined` 恒 true → 有历史也永不跳过 → 任何情况发消息都重插 first-anchor。
|
|
3684
|
-
* 统一经 sessionEvents() 读取(snapshotEvents() 优先,.events 兜底)。
|
|
3685
|
-
*/
|
|
3686
|
-
function hasUserMessageHistory(session) {
|
|
3687
|
-
return sessionEvents(session).some((event) => event.type === "user/message");
|
|
3688
|
-
}
|
|
3689
|
-
/**
|
|
3690
|
-
* 构建一个 epoch 感知晋升跟踪器(纯逻辑,可单测)。
|
|
3691
|
-
* requiredSignals:晋升所需信号数(boundary 后累计;默认 1)。
|
|
3692
|
-
* 多轮锚定(v4):两轮递进锚定时 requiredSignals = 锚定轮数——
|
|
3693
|
-
* 每条锚定回复(assistant/message)计一次,最后一条回复后晋升。
|
|
3694
|
-
*
|
|
3695
|
-
* maxRoundsFallback:**轮次兜底**(v1.19.3 修复 responses API 兼容)。
|
|
3696
|
-
* 某些模型/协议(如 opencode-go-responses)的会话可能不产生标准
|
|
3697
|
-
* `assistant/message` 晋升信号 → 永不晋升 → bootstrap 阶段工具被裁空
|
|
3698
|
-
* (zeroTools 首请求 0 工具,ACC 工具不可见)。兜底:无论晋升信号是否
|
|
3699
|
-
* 到达,观察到的模型 step 数(`step/start` 事件计数,平台稳定事件,
|
|
3700
|
-
* responses/completions 都触发,独立于 promoteEvents)达到 maxRoundsFallback
|
|
3701
|
-
* 即强制 promoted(开放完整工具)。正常模型锚定轮数(requiredSignals)
|
|
3702
|
-
* 通常 ≤ 2 < 默认兜底 3,不受影响。
|
|
3703
|
-
*/
|
|
3704
|
-
function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallback = 3) {
|
|
3705
|
-
/** sessionId -> { boundary, signalCount, rounds }(rounds = 兜底用回复轮计数) */
|
|
3706
|
-
const state = /* @__PURE__ */ new Map();
|
|
3707
|
-
const sessionIdOf = (session) => {
|
|
3708
|
-
if (session && typeof session === "object") {
|
|
3709
|
-
const id = session.id;
|
|
3710
|
-
if (typeof id === "string") return id;
|
|
3711
|
-
}
|
|
3712
|
-
};
|
|
3713
|
-
const promotedBy = (entry) => entry.signalCount >= requiredSignals || entry.rounds >= maxRoundsFallback;
|
|
3714
|
-
const scan = (session) => {
|
|
3715
|
-
let boundary = -1;
|
|
3716
|
-
let signalCount = 0;
|
|
3717
|
-
let rounds = 0;
|
|
3718
|
-
const events = sessionEvents(session);
|
|
3719
|
-
if (events.length > 0) for (const event of events) {
|
|
3720
|
-
const e = event;
|
|
3721
|
-
const seq = typeof e.seq === "number" ? e.seq : 0;
|
|
3722
|
-
if (e.type === "compaction/end") {
|
|
3723
|
-
boundary = seq;
|
|
3724
|
-
signalCount = 0;
|
|
3725
|
-
rounds = 0;
|
|
3726
|
-
continue;
|
|
3727
|
-
}
|
|
3728
|
-
if (promoteEvents.has(e.type) && seq > boundary) signalCount++;
|
|
3729
|
-
if (e.type === "step/start" && seq > boundary) rounds++;
|
|
3730
|
-
}
|
|
3731
|
-
const entry = {
|
|
3732
|
-
boundary,
|
|
3733
|
-
signalCount,
|
|
3734
|
-
rounds
|
|
3735
|
-
};
|
|
3736
|
-
const sid = sessionIdOf(session);
|
|
3737
|
-
if (sid) state.set(sid, entry);
|
|
3738
|
-
return {
|
|
3739
|
-
boundary,
|
|
3740
|
-
promoted: promotedBy(entry)
|
|
3741
|
-
};
|
|
3742
|
-
};
|
|
3743
|
-
return {
|
|
3744
|
-
status(agent) {
|
|
3745
|
-
if (agent === void 0) return {
|
|
3746
|
-
boundary: -1,
|
|
3747
|
-
promoted: true
|
|
3748
|
-
};
|
|
3749
|
-
const session = agent.session;
|
|
3750
|
-
if (session === void 0) return {
|
|
3751
|
-
boundary: -1,
|
|
3752
|
-
promoted: true
|
|
3753
|
-
};
|
|
3754
|
-
const loopSid = session.id;
|
|
3755
|
-
if (typeof loopSid === "string" && (loopSid.startsWith("handyman-") || isSkiffSessionId(loopSid))) return {
|
|
3756
|
-
boundary: -1,
|
|
3757
|
-
promoted: true
|
|
3758
|
-
};
|
|
3759
|
-
if ((session.header?.delegationDepth ?? 0) > 0) return {
|
|
3760
|
-
boundary: -1,
|
|
3761
|
-
promoted: true
|
|
3762
|
-
};
|
|
3763
|
-
const sid = sessionIdOf(session);
|
|
3764
|
-
if (sid === void 0) return {
|
|
3765
|
-
boundary: -1,
|
|
3766
|
-
promoted: true
|
|
3767
|
-
};
|
|
3768
|
-
const s = state.get(sid);
|
|
3769
|
-
if (s) return {
|
|
3770
|
-
boundary: s.boundary,
|
|
3771
|
-
promoted: promotedBy(s)
|
|
3772
|
-
};
|
|
3773
|
-
return scan(session);
|
|
3774
|
-
},
|
|
3775
|
-
observe(session, event) {
|
|
3776
|
-
const sid = sessionIdOf(session);
|
|
3777
|
-
if (sid === void 0) return;
|
|
3778
|
-
const entry = state.get(sid);
|
|
3779
|
-
if (entry === void 0) return;
|
|
3780
|
-
const e = event;
|
|
3781
|
-
const seq = typeof e.seq === "number" ? e.seq : 0;
|
|
3782
|
-
if (e.type === "compaction/end") {
|
|
3783
|
-
state.set(sid, {
|
|
3784
|
-
boundary: seq,
|
|
3785
|
-
signalCount: 0,
|
|
3786
|
-
rounds: 0
|
|
3787
|
-
});
|
|
3788
|
-
return;
|
|
3789
|
-
}
|
|
3790
|
-
if (promoteEvents.has(e.type) && seq > entry.boundary) entry.signalCount++;
|
|
3791
|
-
if (e.type === "step/start" && seq > entry.boundary) entry.rounds++;
|
|
3792
|
-
state.set(sid, entry);
|
|
3793
|
-
}
|
|
3794
|
-
};
|
|
3795
|
-
}
|
|
3796
|
-
const PROMOTE_EVENTS = {
|
|
3797
|
-
"tool-call": /* @__PURE__ */ new Set(["tool/call"]),
|
|
3798
|
-
"assistant-message": /* @__PURE__ */ new Set(["assistant/message"]),
|
|
3799
|
-
either: /* @__PURE__ */ new Set(["tool/call", "assistant/message"])
|
|
3800
|
-
};
|
|
3801
|
-
/** 协议固有 bootstrap 设置:zeroTools=true(首请求 0 工具纯文字锚定轮)、
|
|
3802
|
-
* 晋升信号仅 assistant/message(对齐 zero-anchored-standard)、
|
|
3803
|
-
* requiredSignals = 锚定轮数(2 条消息 → 2 条回复后晋升)。无参——零配置面。 */
|
|
3804
|
-
function resolveBootstrapSettings() {
|
|
3805
|
-
return {
|
|
3806
|
-
bootstrapTools: [...DEFAULT_BOOTSTRAP_TOOLS],
|
|
3807
|
-
promoteEvents: PROMOTE_EVENTS["assistant-message"],
|
|
3808
|
-
requiredSignals: DEFAULT_ANCHOR_MESSAGES.length,
|
|
3809
|
-
suppressedSources: new Set(DEFAULT_SUPPRESSED_SOURCES),
|
|
3810
|
-
compactionTools: [...DEFAULT_COMPACTION_TOOLS],
|
|
3811
|
-
anchorMessages: [...DEFAULT_ANCHOR_MESSAGES],
|
|
3812
|
-
zeroTools: true
|
|
3813
|
-
};
|
|
3814
|
-
}
|
|
3815
|
-
/**
|
|
3816
|
-
* 注册 Anchored bootstrap 机制(协议固有,零配置面——S142;按 agent cwd 定位 CCC root,
|
|
3817
|
-
* 晋升状态按 root 隔离,但设置全局一致):
|
|
3818
|
-
* 1. session/event 观察晋升信号(assistant/message + step/start 兜底 + compaction/end)
|
|
3819
|
-
* 2. system-prompt/assemble:bootstrap 阶段目录窄化到 bootstrapTools(+compaction 后 compactionTools)
|
|
3820
|
-
* 3. agent/pre-step:bootstrap 阶段剥离 suppressedSources 注入消息(降级保留全部)
|
|
3821
|
-
*
|
|
3822
|
-
* 摘除 = 删除 index.ts 中本注册行(独立模块)。
|
|
3823
|
-
*/
|
|
3824
|
-
/** 从 agent 解析 CCC 根(宽松;无 agent/无 CCC 返回 null) */
|
|
3825
|
-
function agentRoot(agent) {
|
|
3826
|
-
const cwd = agent?.session?.header?.cwd;
|
|
3827
|
-
if (typeof cwd !== "string") return null;
|
|
3828
|
-
return findSerenityRoot(cwd);
|
|
3829
|
-
}
|
|
3830
|
-
/** 协议固有设置(零配置面):所有 CCC 使用同一固化设置 */
|
|
3831
|
-
const SETTINGS = resolveBootstrapSettings();
|
|
3832
|
-
function registerBootstrap(ctx) {
|
|
3833
|
-
const trackers = /* @__PURE__ */ new Map();
|
|
3834
|
-
/** 已锚定会话(进程内防重:子 agent 每会话只锚定一次) */
|
|
3835
|
-
const anchoredSessions = /* @__PURE__ */ new Set();
|
|
3836
|
-
const trackerFor = (root) => {
|
|
3837
|
-
let t = trackers.get(root);
|
|
3838
|
-
if (!t) {
|
|
3839
|
-
t = createEpochPromotion(SETTINGS.promoteEvents, SETTINGS.requiredSignals);
|
|
3840
|
-
trackers.set(root, t);
|
|
3841
|
-
}
|
|
3842
|
-
return t;
|
|
3843
|
-
};
|
|
3844
|
-
ctx.on("session/event", (session, event) => {
|
|
3845
|
-
try {
|
|
3846
|
-
const cwd = session?.header?.cwd;
|
|
3847
|
-
if (typeof cwd !== "string") return;
|
|
3848
|
-
const root = findSerenityRoot(cwd);
|
|
3849
|
-
if (!root) return;
|
|
3850
|
-
trackerFor(root).observe(session, event);
|
|
3851
|
-
} catch {}
|
|
3852
|
-
});
|
|
3853
|
-
ctx.on("agent/inbox/inserted", ({ agent, message }) => {
|
|
3854
|
-
try {
|
|
3855
|
-
if (!agentRoot(agent)) return;
|
|
3856
|
-
const session = agent.session;
|
|
3857
|
-
const depth = session?.header?.delegationDepth ?? 0;
|
|
3858
|
-
const sid = typeof session?.id === "string" ? session.id : void 0;
|
|
3859
|
-
if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
|
|
3860
|
-
if (depth === 0) {
|
|
3861
|
-
if (hasUserMessageHistory(session)) return;
|
|
3862
|
-
} else if (sid !== void 0 && anchoredSessions.has(sid)) return;
|
|
3863
|
-
if (message?.source?.kind === "plugin") return;
|
|
3864
|
-
const inbox = agent.inbox;
|
|
3865
|
-
if (!inbox?.prepend) return;
|
|
3866
|
-
for (let i = SETTINGS.anchorMessages.length - 1; i >= 0; i--) inbox.prepend("next-turn", {
|
|
3867
|
-
id: `bootstrap-anchor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}-${i}`,
|
|
3868
|
-
role: "user",
|
|
3869
|
-
content: [{
|
|
3870
|
-
type: "text",
|
|
3871
|
-
text: SETTINGS.anchorMessages[i]
|
|
3872
|
-
}],
|
|
3873
|
-
source: {
|
|
3874
|
-
kind: "plugin",
|
|
3875
|
-
plugin: "dsh-serenity-hooks",
|
|
3876
|
-
form: "notice",
|
|
3877
|
-
summary: "bootstrap anchor turn"
|
|
3878
|
-
}
|
|
3879
|
-
});
|
|
3880
|
-
if (sid !== void 0) anchoredSessions.add(sid);
|
|
3881
|
-
warnOnce(`anchor turns injected: ${SETTINGS.anchorMessages.length} 条(${SETTINGS.anchorMessages[0].slice(0, 40)}…)`);
|
|
3882
|
-
} catch {}
|
|
3883
|
-
});
|
|
3884
|
-
let warned = false;
|
|
3885
|
-
const warnOnce = (message) => {
|
|
3886
|
-
if (warned) return;
|
|
3887
|
-
warned = true;
|
|
3888
|
-
try {
|
|
3889
|
-
console.warn(`[serenity-hooks] bootstrap: ${message}`);
|
|
3890
|
-
} catch {}
|
|
3891
|
-
};
|
|
3892
|
-
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
3893
|
-
const assembled = await next();
|
|
3894
|
-
try {
|
|
3895
|
-
const agent = context.agent;
|
|
3896
|
-
const root = agentRoot(agent);
|
|
3897
|
-
if (!root) return assembled;
|
|
3898
|
-
const status = trackerFor(root).status(agent);
|
|
3899
|
-
if (status.promoted) return assembled;
|
|
3900
|
-
const tools = assembled.tools;
|
|
3901
|
-
if (!Array.isArray(tools)) return assembled;
|
|
3902
|
-
const available = new Set(tools.map((tool) => tool.name).filter((n) => typeof n === "string"));
|
|
3903
|
-
if (SETTINGS.zeroTools) {
|
|
3904
|
-
if (status.boundary < 0) return {
|
|
3905
|
-
...assembled,
|
|
3906
|
-
tools: []
|
|
3907
|
-
};
|
|
3908
|
-
const keep = new Set(SETTINGS.compactionTools);
|
|
3909
|
-
const missing = [...keep].filter((name) => !available.has(name));
|
|
3910
|
-
if (missing.length > 0) {
|
|
3911
|
-
warnOnce(`expected compaction tools missing=${JSON.stringify(missing)} — bootstrap disabled, full catalog exposed`);
|
|
3912
|
-
return assembled;
|
|
3913
|
-
}
|
|
3914
|
-
return {
|
|
3915
|
-
...assembled,
|
|
3916
|
-
tools: tools.filter((tool) => typeof tool.name === "string" && keep.has(tool.name))
|
|
3917
|
-
};
|
|
3918
|
-
}
|
|
3919
|
-
const keep = new Set(SETTINGS.bootstrapTools);
|
|
3920
|
-
if (status.boundary >= 0) for (const toolName of SETTINGS.compactionTools) keep.add(toolName);
|
|
3921
|
-
const missing = [...keep].filter((name) => !available.has(name));
|
|
3922
|
-
if (missing.length > 0) {
|
|
3923
|
-
warnOnce(`expected bootstrap tools missing=${JSON.stringify(missing)} — bootstrap disabled, full catalog exposed`);
|
|
3924
|
-
return assembled;
|
|
3925
|
-
}
|
|
3926
|
-
return {
|
|
3927
|
-
...assembled,
|
|
3928
|
-
tools: tools.filter((tool) => typeof tool.name === "string" && keep.has(tool.name))
|
|
3929
|
-
};
|
|
3930
|
-
} catch (error) {
|
|
3931
|
-
warnOnce(`assemble filter failed, exposing the full catalog: ${String(error?.message ?? error)}`);
|
|
3932
|
-
return assembled;
|
|
3933
|
-
}
|
|
3934
|
-
});
|
|
3935
|
-
ctx.on("agent/pre-step", async ({ agent }, next) => {
|
|
3936
|
-
const decision = await next();
|
|
3937
|
-
if (decision.kind === "reject") return decision;
|
|
3938
|
-
try {
|
|
3939
|
-
const root = agentRoot(agent);
|
|
3940
|
-
if (!root) return decision;
|
|
3941
|
-
if (trackerFor(root).status(agent).promoted || SETTINGS.suppressedSources.size === 0) return decision;
|
|
3942
|
-
const messages = decision.messages;
|
|
3943
|
-
if (!Array.isArray(messages)) return decision;
|
|
3944
|
-
const kept = messages.filter((message) => {
|
|
3945
|
-
const kind = message?.source?.kind;
|
|
3946
|
-
return typeof kind !== "string" || !SETTINGS.suppressedSources.has(kind);
|
|
3947
|
-
});
|
|
3948
|
-
return kept.length === messages.length ? decision : {
|
|
3949
|
-
...decision,
|
|
3950
|
-
messages: kept
|
|
3951
|
-
};
|
|
3952
|
-
} catch (error) {
|
|
3953
|
-
warnOnce(`pre-step context filter failed, keeping injected context: ${String(error?.message ?? error)}`);
|
|
3954
|
-
return decision;
|
|
3955
|
-
}
|
|
3956
|
-
}, { prepend: true });
|
|
3957
|
-
}
|
|
3958
|
-
//#endregion
|
|
3959
|
-
//#region src/trajectory-assistant.ts
|
|
3960
|
-
/**
|
|
3961
|
-
* trajectory-assistant.ts — 关卡化注入的统一命名与风格门面(v0.3,S142)
|
|
3962
|
-
*
|
|
3963
|
-
* 概念(用户拍板 2026-09-05):dsp 全部"过程中动态提示注入"统一命名为
|
|
3964
|
-
* trajectory-assistant(轨迹助航员)。关卡设计思想塑造**结构与时机**
|
|
3965
|
-
* (何时/何地注入),**不用于提示词用词**(D8 词法原则)——纯游戏黑话
|
|
3966
|
-
* (BOSS/XP/level-up)禁止出现在提示词文本;CHECKPOINT/LIMIT/TUTORIAL 等
|
|
3967
|
-
* 跨领域自然通用词允许。
|
|
3968
|
-
*
|
|
3969
|
-
* 本模块 = token 常量 + level-event 词汇表的**单一真相源**(避免前缀散落
|
|
3970
|
-
* 各文件字符串字面量)+ 风格门面(plain 默认 / metaphor 星舰变体)。
|
|
3971
|
-
* 仅当 style=metaphor 时前缀包装措辞变化;可行动正文不变 → 零行为漂移。
|
|
3972
|
-
*
|
|
3973
|
-
* 设计简写(L0 环境 / L1 教程 / L2 目标 / L3 检查点 / L4 极限 / L5 守卫 /
|
|
3974
|
-
* L6 结算)仅在本模块注释与内部文档出现,永不浮现在提示词文本。
|
|
3975
|
-
*
|
|
3976
|
-
* 结算(settlement)挂点:D6 用户拍板——若 CCC 有标准 SESSION 复盘仪式,
|
|
3977
|
-
* 结算视图归 trajectory-assistant。当前 close/archive 无复盘摘要 → 机制
|
|
3978
|
-
* 延后,仅留 onSettlement 导出 seam(OP-1,无调用者)。
|
|
3979
|
-
*/
|
|
3980
|
-
/** 可见提示词前缀用词(跨领域自然词,D8:无游戏黑话) */
|
|
3981
|
-
const EVENT_LABEL = {
|
|
3982
|
-
/** 计分同步提醒(原 TRAJECTORY-STEWARD) */
|
|
3983
|
-
checkpoint: "CHECKPOINT",
|
|
3984
|
-
/** 上下文极限重建提醒(原 TRAJECTORY;LIMIT 替代被否的 BOSS——自然词) */
|
|
3985
|
-
limit: "LIMIT",
|
|
3986
|
-
/** 极限强制升级(原 TRAJECTORY-ESCALATED) */
|
|
3987
|
-
limitMandatory: "LIMIT · MANDATORY",
|
|
3988
|
-
/** 重建锚点头部(原 TRAJECTORY-REBUILD) */
|
|
3989
|
-
rebuild: "REBUILD",
|
|
3990
|
-
/** 敏感输出边界守卫(原 SERENITY OUTPUT GUARD) */
|
|
3991
|
-
guard: "BOUNDARY GUARD"
|
|
3992
|
-
};
|
|
3993
|
-
/** 家族标识(所有动态注入统一前缀) */
|
|
3994
|
-
const ASSISTANT_PREFIX = "TRAJECTORY-ASSISTANT";
|
|
3995
|
-
/** 完整 token:`[TRAJECTORY-ASSISTANT · <LABEL>]` */
|
|
3996
|
-
function eventToken(event) {
|
|
3997
|
-
return `[${ASSISTANT_PREFIX} · ${EVENT_LABEL[event]}]`;
|
|
3998
|
-
}
|
|
3999
|
-
/** ACK 确认码前缀(recorded/skipped 语义不变,仅家族名更新) */
|
|
4000
|
-
const ACK_PREFIX = `${ASSISTANT_PREFIX}-recorded`;
|
|
4001
|
-
//#endregion
|
|
4002
|
-
//#region src/rebuild.ts
|
|
4003
|
-
const PLUGIN_SOURCE$4 = {
|
|
4004
|
-
kind: "plugin",
|
|
4005
|
-
plugin: "dsh-serenity-hooks"
|
|
4006
|
-
};
|
|
4007
|
-
/** acknowledge 尾句(first-anchor 的确认要求——rebuild 重建后不重走确认轮,直接继续) */
|
|
4008
|
-
const ACK_SUFFIX_RE = /Please simply reply "acknowledge" — no action needed\.\s*$/;
|
|
4009
|
-
/**
|
|
4010
|
-
* 剥离 first-anchor 消息的 acknowledge 尾句(rebuild 锚点保留协议正文但不要求确认回复)。
|
|
4011
|
-
*/
|
|
4012
|
-
function stripAckSuffix(text) {
|
|
4013
|
-
return text.replace(ACK_SUFFIX_RE, "").trimEnd();
|
|
4014
|
-
}
|
|
4015
|
-
/**
|
|
4016
|
-
* 构建重建锚点消息(v1.22.4 定稿语义 + v1.22.5 保留 first-anchor 正文):
|
|
4017
|
-
* 「[TRAJECTORY-REBUILD] + first-anchor 协议正文(去 acknowledge 尾句)+ 继续 {SESSION 名} 的工作」。
|
|
4018
|
-
* @param root - CCC 根
|
|
4019
|
-
* @param sessionName - 当前 use 的宁静号 SESSION 名(如 S142);无激活则通用指令
|
|
4020
|
-
* @param activeMdPath - 持久轨迹 SESSION.md 绝对路径(保持原位)
|
|
4021
|
-
* @param anchorMessages - first-anchor 协议消息序列(缺省 DEFAULT_ANCHOR_MESSAGES;可注入测试)
|
|
4022
|
-
*/
|
|
4023
|
-
function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DEFAULT_ANCHOR_MESSAGES) {
|
|
4024
|
-
const rel = activeMdPath.startsWith(root) ? activeMdPath.slice(root.length + 1) : activeMdPath;
|
|
4025
|
-
const sessionDir = basename(dirname(activeMdPath));
|
|
4026
|
-
const sessionLine = sessionDir !== "AGENT_SESSIONS" ? `- Serenity session: ${sessionName !== "" ? `${sessionName} (${sessionDir})` : sessionDir}` : null;
|
|
4027
|
-
return [
|
|
4028
|
-
`${eventToken("rebuild")} The conversation has been cleared and rebuilt (Ship of Theseus: the carrier is replaced, the trajectory continues).`,
|
|
4029
|
-
"",
|
|
4030
|
-
...anchorMessages.flatMap((text) => [stripAckSuffix(text), ""]),
|
|
4031
|
-
sessionName !== "" ? `Continue the work of ${sessionName}.` : "Continue the current work.",
|
|
4032
|
-
...sessionLine ? [sessionLine] : [],
|
|
4033
|
-
`- Persistent trajectory — SESSION.md path: ${rel}`,
|
|
4034
|
-
` (the trajectory's persistent body — stays in place through rebuilds)`,
|
|
4035
|
-
`- Read that SESSION.md first (goal/decisions/progress/unresolved), then continue from the last checkpoint.`
|
|
4036
|
-
].join("\n");
|
|
4037
|
-
}
|
|
4038
|
-
/** 从会话 surface 首条 user 消息(重建锚点)解析 SESSION.md 路径(events 异常/缺失时的兜底) */
|
|
4039
|
-
function parseAnchorMdPath(session) {
|
|
4040
|
-
try {
|
|
4041
|
-
const nodes = [...session.surface.nodes];
|
|
4042
|
-
if (nodes.length === 0) return null;
|
|
4043
|
-
const events = sessionEvents(session);
|
|
4044
|
-
if (events.length === 0) return null;
|
|
4045
|
-
const event = events[nodes[0]];
|
|
4046
|
-
if (!event) return null;
|
|
4047
|
-
const message = deriveEventMessage(event);
|
|
4048
|
-
if (!message) return null;
|
|
4049
|
-
return extractSessionMdPathFromText(message.content?.map((c) => c.type === "text" ? c.text ?? "" : "").join("") ?? "");
|
|
4050
|
-
} catch {
|
|
4051
|
-
return null;
|
|
4052
|
-
}
|
|
4053
|
-
}
|
|
4054
|
-
/** 从 SESSION.md 绝对路径派生会话名(S###;issue 目录回退完整目录名) */
|
|
4055
|
-
function sessionNameFromMdPath(mdPath) {
|
|
4056
|
-
const dirName = basename(dirname(mdPath));
|
|
4057
|
-
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
4058
|
-
return idMatch ? `S${idMatch[1]}` : dirName;
|
|
4059
|
-
}
|
|
4060
|
-
/**
|
|
4061
|
-
* 稳固的 SESSION.md 定位(多层候选 + 存在性校验,**绝不输出虚假路径**):
|
|
4062
|
-
* ① 内存活跃会话(本会话显式 use 过)→ ② events 恢复(use 标记 + 重建锚点规范行,进程重启后)
|
|
4063
|
-
* → ③ surface 首条锚点(events 异常时兜底)→ ④ AGENT_SESSIONS 约定回退(最新未完成活动目录)。
|
|
4064
|
-
* 每候选 resolve 后 existsSync 校验(相对路径按 root 解析);全部失败返回 null → 调用方报错引导。
|
|
4065
|
-
*/
|
|
4066
|
-
function resolveSessionMdPath(root, scope, session) {
|
|
4067
|
-
const candidates = [];
|
|
4068
|
-
candidates.push(getActiveSessionInfo(scope)?.mdPath ?? null);
|
|
4069
|
-
const events = sessionEvents(session);
|
|
4070
|
-
if (events.length > 0) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
|
|
4071
|
-
candidates.push(parseAnchorMdPath(session));
|
|
4072
|
-
candidates.push(findLatestActiveSessionMd(root));
|
|
4073
|
-
for (const c of candidates) {
|
|
4074
|
-
if (!c) continue;
|
|
4075
|
-
const abs = c.startsWith(root) ? c : resolve(root, c);
|
|
4076
|
-
if (existsSync(abs)) return abs;
|
|
4077
|
-
}
|
|
4078
|
-
return null;
|
|
4079
|
-
}
|
|
4080
|
-
const pendingRebuilds = /* @__PURE__ */ new Map();
|
|
4081
|
-
/** 陈旧队列存活时长(毫秒):超过则丢弃(turn 异常结束/agent 崩溃时防残留误清空) */
|
|
4082
|
-
const PENDING_TTL_MS = 6e5;
|
|
4083
|
-
let diagState = {
|
|
4084
|
-
lastTs: "",
|
|
4085
|
-
lastSessionId: "",
|
|
4086
|
-
lastEvent: "",
|
|
4087
|
-
queueCount: 0,
|
|
4088
|
-
rebuiltCount: 0,
|
|
4089
|
-
droppedCount: 0,
|
|
4090
|
-
failedCount: 0
|
|
4091
|
-
};
|
|
4092
|
-
function writeRebuildDiag(root, entry) {
|
|
4093
|
-
try {
|
|
4094
|
-
const dir = join(root, "AGENT_SESSIONS");
|
|
4095
|
-
mkdirSync(dir, { recursive: true });
|
|
4096
|
-
const file = join(dir, ".rebuild-diag.json");
|
|
4097
|
-
const s = diagState;
|
|
4098
|
-
if (entry.event === "queued") s.queueCount += 1;
|
|
4099
|
-
if (entry.event === "rebuilt") s.rebuiltCount += 1;
|
|
4100
|
-
if (entry.event === "ttl-dropped") s.droppedCount += 1;
|
|
4101
|
-
if (entry.event === "failed") s.failedCount += 1;
|
|
4102
|
-
s.lastTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
4103
|
-
s.lastSessionId = entry.sessionId;
|
|
4104
|
-
s.lastEvent = entry.event;
|
|
4105
|
-
s.detail = entry.detail;
|
|
4106
|
-
writeFileSync(file, JSON.stringify(s, null, 2) + "\n", "utf-8");
|
|
4107
|
-
} catch {}
|
|
4108
|
-
}
|
|
4109
|
-
/**
|
|
4110
|
-
* 排队一次重建(v1.22.4 定稿语义第一步):
|
|
4111
|
-
* ① 门控校验(rebuild.enabled + 会话定位)
|
|
4112
|
-
* ② 构建锚点(激活会话名 + 持久轨迹路径)
|
|
4113
|
-
* ③ 写入 pending 队列——**不立即改 surface**(等 turn-stopping 执行)
|
|
4114
|
-
* @returns 排队结果;rebuild.enabled=false 或非 CCC 抛错
|
|
4115
|
-
*/
|
|
4116
|
-
async function queueRebuild(ctx, opts) {
|
|
4117
|
-
if (!readSimpleSettings().rebuildEnabled) throw new Error("session_rebuild is disabled (rebuild.enabled=false — enable it in the dsh settings panel)");
|
|
4118
|
-
const { root, note, summary, dshSessionId } = opts;
|
|
4119
|
-
const session = ctx.sessions?.get?.(dshSessionId);
|
|
4120
|
-
if (!session) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
|
|
4121
|
-
const mdPath = resolveSessionMdPath(root, dshSessionId, session);
|
|
4122
|
-
if (!mdPath) throw new Error("Unable to determine the active SESSION.md — no session context found in this conversation. Run \"session use <S###> --summary <内容概括 ≤20 字>\" first to activate the trajectory to resume, then retry session_rebuild.");
|
|
4123
|
-
const sessionName = getActiveSessionInfo(dshSessionId)?.sessionId ?? sessionNameFromMdPath(mdPath);
|
|
4124
|
-
const anchor = buildRebuildAnchor(root, sessionName, mdPath);
|
|
4125
|
-
appendBound(session, "rebuild", {
|
|
4126
|
-
dirName: basename(dirname(mdPath)),
|
|
4127
|
-
mdPath,
|
|
4128
|
-
sessionId: sessionName.startsWith("S") ? sessionName : void 0,
|
|
4129
|
-
note: "rebuild queued"
|
|
4130
|
-
});
|
|
4131
|
-
pendingRebuilds.set(dshSessionId, {
|
|
4132
|
-
anchor,
|
|
4133
|
-
summary,
|
|
4134
|
-
mdPath,
|
|
4135
|
-
queuedAt: Date.now()
|
|
4136
|
-
});
|
|
4137
|
-
writeRebuildDiag(root, {
|
|
4138
|
-
sessionId: dshSessionId,
|
|
4139
|
-
event: "queued"
|
|
4140
|
-
});
|
|
4141
|
-
return {
|
|
4142
|
-
queued: true,
|
|
4143
|
-
anchor,
|
|
4144
|
-
sessionMdPath: mdPath
|
|
4145
|
-
};
|
|
4146
|
-
}
|
|
4147
|
-
/**
|
|
4148
|
-
* 执行真正的 surface replace(v1.22.4 定稿语义第二步,turn-stopping 时调用):
|
|
4149
|
-
* 当前 turn 已完整结束(所有 tool/result 已 append)→ 全部节点替换为锚点消息。
|
|
4150
|
-
* 同一会话 id 原地重建——同工作区天然满足,无新/旧会话之分。
|
|
4151
|
-
*
|
|
4152
|
-
* **shadow-price 协议(v1.23.5 修复,S142 用户问询)**:DSH token-meter 的
|
|
4153
|
-
* `foldSurfaceProjection` 对 surface `replace` 要求**紧邻其前的 metering 事件**
|
|
4154
|
-
* (`compaction/summary` 或 `compaction/prune`)声明被替换范围的启发式 token 价,
|
|
4155
|
-
* 否则 claim 缺失 → `deltaTokens=0` → `contextBreakdown.messageTokens` 永不扣减
|
|
4156
|
-
* → UI「对话消息」虚高累计、往复 rebuild 计量爆炸(用户截图实证:总 15% 但
|
|
4157
|
-
* 对话消息 ~1M)。官方先例 = `compaction-tool-result-pruner`(append
|
|
4158
|
-
* `compaction/prune` → 紧接 replace)。此处同款:先 append `compaction/prune`
|
|
4159
|
-
* (定价全部被替换节点,经 ctx.tokenMeter.estimateMessage 逐节点累加),再
|
|
4160
|
-
* append 锚点 replace——meter 不可用时退化(无 claim,仅计量漂移,会话功能不受影响)。
|
|
4161
|
-
*
|
|
4162
|
-
* @returns 是否执行了 replace(false = 无 pending 或 surface 空)
|
|
4163
|
-
*/
|
|
4164
|
-
function performRebuild(session, pending, meter) {
|
|
4165
|
-
const nodes = [...session.surface.nodes];
|
|
4166
|
-
if (nodes.length === 0) return false;
|
|
4167
|
-
if (meter) {
|
|
4168
|
-
let shadowedTokenCount = 0;
|
|
4169
|
-
const events = sessionEvents(session);
|
|
4170
|
-
for (const seq of nodes) {
|
|
4171
|
-
const event = events[seq];
|
|
4172
|
-
if (!event) continue;
|
|
4173
|
-
const message = deriveEventMessage(event);
|
|
4174
|
-
if (message) shadowedTokenCount += meter.estimateMessage(message);
|
|
4175
|
-
}
|
|
4176
|
-
session.append("compaction/prune", {
|
|
4177
|
-
shadowedRange: {
|
|
4178
|
-
start: nodes[0],
|
|
4179
|
-
end: nodes[nodes.length - 1]
|
|
4180
|
-
},
|
|
4181
|
-
shadowedSeqs: nodes,
|
|
4182
|
-
shadowedTokenCount
|
|
4183
|
-
});
|
|
4184
|
-
}
|
|
4185
|
-
session.append("user/message", {
|
|
4186
|
-
content: [{
|
|
4187
|
-
type: "text",
|
|
4188
|
-
text: pending.anchor
|
|
4189
|
-
}],
|
|
4190
|
-
source: { kind: "user" }
|
|
4191
|
-
}, {
|
|
4192
|
-
surfaceOp: {
|
|
4193
|
-
op: "replace",
|
|
4194
|
-
start: nodes[0],
|
|
4195
|
-
end: nodes[nodes.length - 1]
|
|
4196
|
-
},
|
|
4197
|
-
sourceEventSeqs: nodes
|
|
4198
|
-
});
|
|
4199
|
-
return true;
|
|
4200
|
-
}
|
|
4201
|
-
/**
|
|
4202
|
-
* 重建后重命名 dsh 会话标题(需求② S142 用户拍板:rebuild 后标题带新阶段概括)。
|
|
4203
|
-
* 标题 = S###-YYYY-MM-DD-<summary>——S### 与日期从持久轨迹目录名派生(不信任 LLM),
|
|
4204
|
-
* 概括来自 queueRebuild 的 summary 参数(服务端清洗截断)。
|
|
4205
|
-
* 失败仅 warn 不阻断重建主流程(标题美观非关键路径)。
|
|
4206
|
-
*/
|
|
4207
|
-
function renameAfterRebuild(ctx, agent, pending) {
|
|
4208
|
-
try {
|
|
4209
|
-
const mdPath = pending.mdPath;
|
|
4210
|
-
const dirName = basename(dirname(mdPath));
|
|
4211
|
-
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
4212
|
-
const title = namingTitleFor({
|
|
4213
|
-
sessionId: idMatch ? `S${idMatch[1]}` : dirName.replace(/^\d{4}-\d{2}-\d{2}--/, ""),
|
|
4214
|
-
dirName,
|
|
4215
|
-
mdPath
|
|
4216
|
-
}, pending.summary);
|
|
4217
|
-
const titles = ctx.get?.("sessionTitle");
|
|
4218
|
-
if (!titles || typeof titles.rename !== "function") {
|
|
4219
|
-
console.warn("[serenity-hooks] rebuild 后重命名跳过: sessionTitle 服务不可用");
|
|
4220
|
-
return;
|
|
4221
|
-
}
|
|
4222
|
-
titles.rename(agent.session, title);
|
|
4223
|
-
console.log(`[serenity-hooks] rebuild 后会话重命名: ${agent.id} → ${title}`);
|
|
4224
|
-
} catch (error) {
|
|
4225
|
-
console.warn(`[serenity-hooks] rebuild 后重命名失败(不阻断): ${String(error?.message ?? error)}`);
|
|
4226
|
-
}
|
|
4227
|
-
}
|
|
4228
|
-
/**
|
|
4229
|
-
* 注册 turn-stopping 钩子(index.ts apply 调用):
|
|
4230
|
-
* agent 每轮 turn 结束前(serial)检查 pending 队列——有该会话的重建请求 → 执行清空
|
|
4231
|
-
* 并 **steer 自动继续**(v1.22.5:next-step 非空 → turn 不 break → 模型自动读取
|
|
4232
|
-
* SESSION.md 继续,无需用户手工输入;DSH 官方先例 hooks-claude-code Stop hook)。
|
|
4233
|
-
* 陈旧队列(超 TTL)丢弃防误清空;无 pending 零开销。
|
|
4234
|
-
*/
|
|
4235
|
-
function registerRebuildTurnHook(ctx) {
|
|
4236
|
-
ctx.on("agent/turn-stopping", (payload) => {
|
|
4237
|
-
const agent = payload?.agent;
|
|
4238
|
-
if (!agent) return;
|
|
4239
|
-
const id = agent.id;
|
|
4240
|
-
const pending = pendingRebuilds.get(id);
|
|
4241
|
-
if (pending === void 0) return;
|
|
4242
|
-
if (Date.now() - pending.queuedAt > PENDING_TTL_MS) {
|
|
4243
|
-
pendingRebuilds.delete(id);
|
|
4244
|
-
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4245
|
-
sessionId: id,
|
|
4246
|
-
event: "ttl-dropped",
|
|
4247
|
-
detail: `queuedAt=${new Date(pending.queuedAt).toISOString()} older than ${PENDING_TTL_MS / 6e4}min TTL — turn did not end in time`
|
|
4248
|
-
});
|
|
4249
|
-
return;
|
|
4250
|
-
}
|
|
4251
|
-
pendingRebuilds.delete(id);
|
|
4252
|
-
try {
|
|
4253
|
-
const tokenMeter = ctx.get?.("tokenMeter");
|
|
4254
|
-
const meter = tokenMeter && typeof tokenMeter.estimateMessage === "function" ? { estimateMessage: (m) => tokenMeter.estimateMessage(m) } : void 0;
|
|
4255
|
-
if (performRebuild(agent.session, pending, meter)) {
|
|
4256
|
-
renameAfterRebuild(ctx, agent, pending);
|
|
4257
|
-
agent.steer(createUserMessage({
|
|
4258
|
-
content: [{
|
|
4259
|
-
type: "text",
|
|
4260
|
-
text: `${eventToken("rebuild")} The conversation has been cleared and rebuilt. Follow the anchor instructions above now: read the persistent trajectory (SESSION.md) and continue the work automatically from the last checkpoint.`
|
|
4261
|
-
}],
|
|
4262
|
-
source: PLUGIN_SOURCE$4
|
|
4263
|
-
}));
|
|
4264
|
-
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4265
|
-
sessionId: id,
|
|
4266
|
-
event: "rebuilt"
|
|
4267
|
-
});
|
|
4268
|
-
console.log(`[serenity-hooks] session_rebuild executed with auto-continue (turn ${payload.turn ?? "?"} ended): ${id}`);
|
|
4269
|
-
} else writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4270
|
-
sessionId: id,
|
|
4271
|
-
event: "empty-surface"
|
|
4272
|
-
});
|
|
4273
|
-
} catch (error) {
|
|
4274
|
-
const msg = String(error?.message ?? error);
|
|
4275
|
-
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
4276
|
-
sessionId: id,
|
|
4277
|
-
event: "failed",
|
|
4278
|
-
detail: msg
|
|
4279
|
-
});
|
|
4280
|
-
console.warn(`[serenity-hooks] session_rebuild failed: ${msg}`);
|
|
4281
|
-
}
|
|
4282
|
-
});
|
|
4283
|
-
}
|
|
4284
|
-
/** 从 agent 会话 cwd 解析 CCC 根(诊断落盘用);无则回退进程 cwd */
|
|
4285
|
-
function resolveSerenityRootFor(agent) {
|
|
4286
|
-
const cwd = agent.session?.header?.cwd;
|
|
4287
|
-
return findSerenityRoot(cwd ?? process.cwd()) ?? process.cwd();
|
|
4288
|
-
}
|
|
4289
|
-
//#endregion
|
|
4290
|
-
//#region src/tools/rebuild.ts
|
|
4291
|
-
/**
|
|
4292
|
-
* rebuild.ts — session_rebuild 工具定义(轨迹跟踪器,v1.22.4 定稿:复用旧会话 + 完全清空重来)
|
|
4293
|
-
*
|
|
4294
|
-
* LLM 主动触发清空重建(用户决策:keeper 超阈值提示 [TRAJECTORY],不自动执行——防误清空)。
|
|
4295
|
-
* v1.22.4 定稿语义(用户明确):**复用当前 dsh 会话(同一 id),turn 结束时 surface
|
|
4296
|
-
* 完全清空重建**——锚点消息「继续 {宁静号 SESSION 名} 的工作」;SESSION.md 是持久轨迹
|
|
4297
|
-
* 永远原位,身份从 SESSION.md 自动延续。同一会话 id → 同工作区天然满足、无新/旧会话
|
|
4298
|
-
* 之分、无需销毁/切换/归档。
|
|
4299
|
-
* v1.22.5 增强(S142 用户需求):① turn-stopping 执行 replace 后 **steer 自动继续**
|
|
4300
|
-
* (无需用户手工输入,模型自动读 SESSION.md 接续);② 锚点消息**保留 first-anchor
|
|
4301
|
-
* 协议正文**(ACC 身份/EAP/协作协议,去 acknowledge 尾句——重建后直接干活)。
|
|
4302
|
-
* 执行时机:工具只排队(queueRebuild),`agent/turn-stopping` 钩子执行真正 replace
|
|
4303
|
-
* (turn 结束前所有 tool/result 已 append → 无孤儿;S141 INVALID_REQUEST 根治)。
|
|
4304
|
-
* 服务端逻辑在 ../rebuild.ts(queueRebuild/performRebuild/registerRebuildTurnHook)。
|
|
4305
|
-
*/
|
|
4306
|
-
function agentCwd$4(exec) {
|
|
4307
|
-
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
4308
|
-
}
|
|
4309
|
-
function agentSessionId(exec) {
|
|
4310
|
-
return exec.agent?.session?.id ?? "";
|
|
4311
|
-
}
|
|
4312
|
-
function renderText$3(value) {
|
|
4313
|
-
return [{
|
|
4314
|
-
type: "text",
|
|
4315
|
-
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
4316
|
-
}];
|
|
4317
|
-
}
|
|
4318
|
-
/** 创建 session_rebuild 工具(闭包捕获 ctx → 排队清空重建,turn 结束时同一会话原地重来并自动继续) */
|
|
4319
|
-
function createRebuildTool(ctx) {
|
|
4320
|
-
return defineTool({
|
|
4321
|
-
name: "session_rebuild",
|
|
4322
|
-
description: "Trajectory-tracker overflow rebuild (Ship of Theseus): this session is the rebuildable carrier of a trajectory — the current conversation is discarded and rebuilt in place at the end of this turn with the anchor \"continue the work of {SESSION name}\" (first-anchor protocol body included), then auto-continues. SESSION.md (the trajectory's persistent body) stays in place; identity continues from it. Use when you receive a " + eventToken("limit") + " reminder (context above threshold), at a natural pause point. After triggering, resume from SESSION.md when this turn ends.",
|
|
4323
|
-
parameters: {
|
|
4324
|
-
note: {
|
|
4325
|
-
type: "string",
|
|
4326
|
-
description: "Optional: one-sentence rebuild background note (for the rebuilt self)"
|
|
4327
|
-
},
|
|
4328
|
-
summary: {
|
|
4329
|
-
type: "string",
|
|
4330
|
-
description: "REQUIRED: content summary ≤20 chars of the next work phase — the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after the rebuild (the rebuilt phase gets a fresh summary); sanitized/truncated server-side"
|
|
4331
|
-
}
|
|
4332
|
-
},
|
|
4333
|
-
output: {
|
|
4334
|
-
schema: { type: "json" },
|
|
4335
|
-
render: (args, value) => renderText$3(value)
|
|
4336
|
-
},
|
|
4337
|
-
async execute(args, exec) {
|
|
4338
|
-
const root = findSerenityRoot(agentCwd$4(exec));
|
|
4339
|
-
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
4340
|
-
const dshSessionId = agentSessionId(exec);
|
|
4341
|
-
if (!dshSessionId) throw new Error("Unable to determine the current dsh session id");
|
|
4342
|
-
if (!args.summary || args.summary.trim() === "") throw new Error("session_rebuild requires --summary <content summary ≤20 chars> (the dsh session title is renamed after rebuild to S###-YYYY-MM-DD-<summary>)");
|
|
4343
|
-
const result = await queueRebuild(ctx, {
|
|
4344
|
-
root,
|
|
4345
|
-
note: args.note,
|
|
4346
|
-
summary: args.summary,
|
|
4347
|
-
agentCwd: agentCwd$4(exec),
|
|
4348
|
-
dshSessionId
|
|
4349
|
-
});
|
|
4350
|
-
return {
|
|
4351
|
-
ok: true,
|
|
4352
|
-
queued: result.queued,
|
|
4353
|
-
anchor: result.anchor,
|
|
4354
|
-
sessionMdPath: result.sessionMdPath,
|
|
4355
|
-
instruction: "Queued clear-and-rebuild: when this turn ends, the same conversation will be cleared and injected with the \"continue the work\" anchor (first-anchor protocol body included), then auto-continue — no manual input needed; resume from SESSION.md at that point."
|
|
4356
|
-
};
|
|
4357
|
-
}
|
|
4358
|
-
});
|
|
4359
|
-
}
|
|
4360
|
-
//#endregion
|
|
4361
|
-
//#region src/tools/localstore.ts
|
|
3011
|
+
//#region src/tools/localstore.ts
|
|
4362
3012
|
/**
|
|
4363
3013
|
* localstore.ts — localstore 真实 DSH 工具定义(defineTool)
|
|
4364
3014
|
*
|
|
@@ -4385,7 +3035,7 @@ function renderText$2(value) {
|
|
|
4385
3035
|
}
|
|
4386
3036
|
const localstoreTool = defineTool({
|
|
4387
3037
|
name: "localstore",
|
|
4388
|
-
description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee),
|
|
3038
|
+
description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee), container_git commit checks and refuses. Subcommands: list (list keys, credentials never return values) / get <name> (read value) / set <name> <value> (write) / unset <name> (delete) / show <name> (metadata, credentials never print values) / doc (output the storage spec — path/format/key conventions/git policy; agents may operate the file directly with read/write per the spec). Default scope=credential; config requires --scope config (path is section.key, e.g. handyman.models).",
|
|
4389
3039
|
parameters: {
|
|
4390
3040
|
action: {
|
|
4391
3041
|
type: "string",
|
|
@@ -4419,23 +3069,6 @@ const localstoreTool = defineTool({
|
|
|
4419
3069
|
});
|
|
4420
3070
|
//#endregion
|
|
4421
3071
|
//#region src/tools/skiff-admin.ts
|
|
4422
|
-
/**
|
|
4423
|
-
* skiff-admin.ts — skiff_admin ACC 工具(F4a',v1.25.0 实验性,第 12 个工具)
|
|
4424
|
-
*
|
|
4425
|
-
* 教 CCC 如何定义 Skiff 角色(仿 session 工具 hook-develop-guide 的 SEP 教学模式,
|
|
4426
|
-
* 用户拍板 2026-08-28:guide 定义教程 / validate 配置校验 / list 角色摘要)。
|
|
4427
|
-
*
|
|
4428
|
-
* 归属:ACC 机制工具(教会 + 校验),角色内容仍归 CCC 配置(.opencode/serenity.json skiff.roles)。
|
|
4429
|
-
*/
|
|
4430
|
-
function agentCwd$2(exec) {
|
|
4431
|
-
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
4432
|
-
}
|
|
4433
|
-
function renderText$1(value) {
|
|
4434
|
-
return [{
|
|
4435
|
-
type: "text",
|
|
4436
|
-
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
4437
|
-
}];
|
|
4438
|
-
}
|
|
4439
3072
|
/** Skiff 定义教程(核心):概念 / schema / 认知 MSM 写法 / 双白名单 / 轨迹纪律 / 示例角色 */
|
|
4440
3073
|
const SKIFF_GUIDE = `═══ Skiff Definition Guide (F4, experimental) ═══
|
|
4441
3074
|
|
|
@@ -4447,7 +3080,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
4447
3080
|
{ "skiff": { "roles": {
|
|
4448
3081
|
"<role-name>": {
|
|
4449
3082
|
"model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 handyman.defaultModel)
|
|
4450
|
-
"msms": ["msm-a", "msm-b"], // MSM 白名单(独立):
|
|
3083
|
+
"msms": ["msm-a", "msm-b"], // MSM 白名单(独立):msm 工具只能执行这些;register/deregister 必拒
|
|
4451
3084
|
"tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
|
|
4452
3085
|
"trajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
|
|
4453
3086
|
// 角色人格/认知边界/风格(CCC 完整定义;dsp 只给基础提示词)
|
|
@@ -4458,7 +3091,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
4458
3091
|
|
|
4459
3092
|
── 双白名单语义(全按白名单暴露,白名单外全隐藏)──
|
|
4460
3093
|
- tools 空 + msms 非空 = 纯 MSM 角色(认知问答典型形态)
|
|
4461
|
-
- msms 非空 →
|
|
3094
|
+
- msms 非空 → msm 工具自动可用(MSM 通道)
|
|
4462
3095
|
- 白名单外工具即使 DSH 未来新增也自动被挡(guard 按角色判定,不枚举工具名——完备性)
|
|
4463
3096
|
|
|
4464
3097
|
── 认知 MSM 写法(读知识 / 操作能力)──
|
|
@@ -4486,7 +3119,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
|
|
|
4486
3119
|
session/cancel / session/close / session/list / request_permission(恒 allow)
|
|
4487
3120
|
启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
|
|
4488
3121
|
生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
|
|
4489
|
-
改配置后用
|
|
3122
|
+
改配置后用 container_admin role apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
|
|
4490
3123
|
会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
|
|
4491
3124
|
「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
|
|
4492
3125
|
注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
|
|
@@ -4559,7 +3192,7 @@ function applySkiffConfig(root) {
|
|
|
4559
3192
|
cccRoot: root,
|
|
4560
3193
|
roleCount: roles.size,
|
|
4561
3194
|
issues: validation.issues,
|
|
4562
|
-
hint: "fix the issues above, then run
|
|
3195
|
+
hint: "fix the issues above, then run container_admin role apply again"
|
|
4563
3196
|
};
|
|
4564
3197
|
return {
|
|
4565
3198
|
applied: true,
|
|
@@ -4569,20 +3202,81 @@ function applySkiffConfig(root) {
|
|
|
4569
3202
|
note: "roles are read live from .opencode/serenity.json — the skiff debug page reflects changes on refresh; skill loading is always available to skiff sessions (not whitelisted)"
|
|
4570
3203
|
};
|
|
4571
3204
|
}
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
3205
|
+
//#endregion
|
|
3206
|
+
//#region src/tools/container-admin.ts
|
|
3207
|
+
/**
|
|
3208
|
+
* container-admin.ts — container_admin(v1.30:机务舱——容器管理面统一入口)
|
|
3209
|
+
*
|
|
3210
|
+
* 合并自:
|
|
3211
|
+
* · skiff_admin(F4 Skiff 认知子集角色:guide/validate/apply/list)
|
|
3212
|
+
* · acc_msm 管理面(register/deregister/check/guide/catalog/ccc-config)
|
|
3213
|
+
* —— msm 工具只管执行+发现;一切「管理容器」的操作归本工具
|
|
3214
|
+
*
|
|
3215
|
+
* 语义(用户拍板"skiff_admin 及系列 admin 合并成 container_admin"):
|
|
3216
|
+
* container = CCC(具体认知容器);admin = 管理操作。
|
|
3217
|
+
* 类比 The Manifest(清单维护——工具注册表)+ Crew(角色管理)+ 机务(配置/手册)。
|
|
3218
|
+
*
|
|
3219
|
+
* 参数极简(LLM 直觉):domain(管理域)+ action(该域子命令)+ 少量业务参数。
|
|
3220
|
+
*/
|
|
3221
|
+
function agentCwd$2(exec) {
|
|
3222
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3223
|
+
}
|
|
3224
|
+
function agentSessionId(exec) {
|
|
3225
|
+
return exec.agent?.session?.id ?? "";
|
|
3226
|
+
}
|
|
3227
|
+
function renderText$1(value) {
|
|
3228
|
+
return [{
|
|
3229
|
+
type: "text",
|
|
3230
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
3231
|
+
}];
|
|
3232
|
+
}
|
|
3233
|
+
const containerAdminTool = defineTool({
|
|
3234
|
+
name: "container_admin",
|
|
3235
|
+
description: "Container administration (the ship's maintenance bay — one entry for managing the CCC): role — Skiff cognitive-subset roles (guide/validate/apply/list); msm — MSM registry management (register/deregister/check quality DC-M1~M4) + manuals (guide dev manual / catalog ACC capability directory / ccc-config CCC configuration reference); config — view CCC configuration. Execution of registered MSMs belongs to the msm tool; this tool manages the container.",
|
|
3236
|
+
parameters: {
|
|
3237
|
+
domain: {
|
|
3238
|
+
type: "string",
|
|
3239
|
+
enum: [
|
|
3240
|
+
"role",
|
|
3241
|
+
"msm",
|
|
3242
|
+
"config"
|
|
3243
|
+
],
|
|
3244
|
+
required: true,
|
|
3245
|
+
description: "Management domain: role (Skiff roles) / msm (MSM registry + manuals) / config (CCC config view)"
|
|
3246
|
+
},
|
|
3247
|
+
action: {
|
|
3248
|
+
type: "string",
|
|
3249
|
+
description: "Domain subcommand (see domain descriptions; e.g. role: validate/apply/list/guide; msm: register/deregister/check/guide/catalog/ccc-config)"
|
|
3250
|
+
},
|
|
3251
|
+
name: {
|
|
3252
|
+
type: "string",
|
|
3253
|
+
description: "MSM name (register/deregister)"
|
|
3254
|
+
},
|
|
3255
|
+
path: {
|
|
3256
|
+
type: "string",
|
|
3257
|
+
description: "register: script relative path inside root"
|
|
3258
|
+
},
|
|
3259
|
+
category: {
|
|
3260
|
+
type: "string",
|
|
3261
|
+
description: "register: mech | semi-mech"
|
|
3262
|
+
},
|
|
3263
|
+
description: {
|
|
3264
|
+
type: "string",
|
|
3265
|
+
description: "register: one-line description"
|
|
3266
|
+
},
|
|
3267
|
+
skill: {
|
|
3268
|
+
type: "string",
|
|
3269
|
+
description: "register: owning skill"
|
|
3270
|
+
},
|
|
3271
|
+
flags: {
|
|
3272
|
+
type: "string",
|
|
3273
|
+
description: "register: flags JSON string (e.g. [{\"name\":\"x\",\"type\":\"string\"}] — type:\"path\" enables escape guard)"
|
|
3274
|
+
},
|
|
3275
|
+
usage: {
|
|
3276
|
+
type: "string",
|
|
3277
|
+
description: "register: custom usage (auto-generated by default)"
|
|
3278
|
+
}
|
|
3279
|
+
},
|
|
4586
3280
|
output: {
|
|
4587
3281
|
schema: { type: "json" },
|
|
4588
3282
|
render: (args, value) => renderText$1(value)
|
|
@@ -4590,13 +3284,43 @@ const skiffAdminTool = defineTool({
|
|
|
4590
3284
|
async execute(args, exec) {
|
|
4591
3285
|
const root = findSerenityRoot(agentCwd$2(exec));
|
|
4592
3286
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
4593
|
-
|
|
3287
|
+
const sessionId = agentSessionId(exec);
|
|
3288
|
+
const domain = args.domain;
|
|
3289
|
+
const action = args.action ?? "";
|
|
3290
|
+
if (domain === "msm" && (action === "register" || action === "deregister")) {
|
|
3291
|
+
const gate = skiffMsmGate(root, sessionId, action, args.name);
|
|
3292
|
+
if (gate.reject) throw new Error(gate.reject);
|
|
3293
|
+
}
|
|
3294
|
+
if (domain === "role") switch (action) {
|
|
4594
3295
|
case "guide": return { guide: SKIFF_GUIDE };
|
|
4595
3296
|
case "validate": return validateSkiffConfig(root);
|
|
4596
3297
|
case "apply": return applySkiffConfig(root);
|
|
4597
3298
|
case "list": return listSkiffRoles(root);
|
|
4598
|
-
default: throw new Error(
|
|
3299
|
+
default: throw new Error(`container_admin role requires action: guide | validate | apply | list`);
|
|
3300
|
+
}
|
|
3301
|
+
if (domain === "msm") {
|
|
3302
|
+
const mgmtActions = [
|
|
3303
|
+
"register",
|
|
3304
|
+
"deregister",
|
|
3305
|
+
"check",
|
|
3306
|
+
"guide",
|
|
3307
|
+
"catalog",
|
|
3308
|
+
"ccc-config"
|
|
3309
|
+
];
|
|
3310
|
+
if (!mgmtActions.includes(action)) throw new Error(`container_admin msm requires action: ${mgmtActions.join(" | ")} (execution goes through the msm tool)`);
|
|
3311
|
+
return runMsm(root, {
|
|
3312
|
+
action,
|
|
3313
|
+
name: args.name,
|
|
3314
|
+
skill: args.skill,
|
|
3315
|
+
path: args.path,
|
|
3316
|
+
category: args.category,
|
|
3317
|
+
description: args.description,
|
|
3318
|
+
flags: args.flags,
|
|
3319
|
+
usage: args.usage
|
|
3320
|
+
});
|
|
4599
3321
|
}
|
|
3322
|
+
if (domain === "config") return { config: runMsm(root, { action: "ccc-config" }) };
|
|
3323
|
+
throw new Error(`container_admin requires domain: role | msm | config`);
|
|
4600
3324
|
}
|
|
4601
3325
|
});
|
|
4602
3326
|
//#endregion
|
|
@@ -4630,7 +3354,7 @@ var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
|
|
|
4630
3354
|
shouldWake: () => shouldWake,
|
|
4631
3355
|
wakeHistoryFor: () => wakeHistoryFor
|
|
4632
3356
|
});
|
|
4633
|
-
const PLUGIN_SOURCE$
|
|
3357
|
+
const PLUGIN_SOURCE$4 = {
|
|
4634
3358
|
kind: "plugin",
|
|
4635
3359
|
plugin: "dsh-serenity-hooks"
|
|
4636
3360
|
};
|
|
@@ -4744,7 +3468,7 @@ async function fetchBiasContent(root, providerRel) {
|
|
|
4744
3468
|
}
|
|
4745
3469
|
if (!existsSync(scriptAbs)) return {
|
|
4746
3470
|
text: null,
|
|
4747
|
-
error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(或旧默认 ${LEGACY_BIAS_PROVIDER};stdout 输出偏见内容一行;
|
|
3471
|
+
error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(或旧默认 ${LEGACY_BIAS_PROVIDER};stdout 输出偏见内容一行;msm autopilot-trajectory init 可生成模板)`
|
|
4748
3472
|
};
|
|
4749
3473
|
const runs = [["bun", [scriptAbs]], [process.execPath, [scriptAbs]]];
|
|
4750
3474
|
for (const [cmd, args] of runs) try {
|
|
@@ -4868,401 +3592,795 @@ async function performAutopilotWake(ctx, root, settings, opts = {}) {
|
|
|
4868
3592
|
type: "text",
|
|
4869
3593
|
text: message
|
|
4870
3594
|
}],
|
|
4871
|
-
source: PLUGIN_SOURCE$
|
|
3595
|
+
source: PLUGIN_SOURCE$4
|
|
4872
3596
|
}));
|
|
4873
3597
|
return {
|
|
4874
|
-
ok: true,
|
|
4875
|
-
detail: `已唤起 ${basename(dirname(mdPath))}(偏见提供者 ${provider})`
|
|
3598
|
+
ok: true,
|
|
3599
|
+
detail: `已唤起 ${basename(dirname(mdPath))}(偏见提供者 ${provider})`
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
/**
|
|
3603
|
+
* 装配(index.ts apply 调用)。时钟唤起(v1.26.14 修复 + v1.27.4 多 CCC 独立):
|
|
3604
|
+
*
|
|
3605
|
+
* v1.26.14 根因:旧实现启动时一次性解析 root——web 进程启动时 live 会话往往为空 → 定时器不启动。
|
|
3606
|
+
* 修复:① 每次 tick 动态解析 root+settings ② 监听 session/created 启动定时器 ③ 优先实验 CCC。
|
|
3607
|
+
*
|
|
3608
|
+
* **v1.27.4 多 CCC 独立(用户"4个CCC能各自有autotrajectory吗")**:
|
|
3609
|
+
* - 单定时器保留(TICK_MS 10min),每次 tick **遍历所有 live+enabled CCC**(collectAutopilotCccs)
|
|
3610
|
+
* 各自评估 shouldWake + 各自唤起——每 CCC 的 interval/session/bias/topPrompt/窗口独立
|
|
3611
|
+
* - `running` 守卫 **per-CCC**(不同 CCC 唤起互不阻塞)
|
|
3612
|
+
* - **全局串行化**(wakeChain:同 tick 多 CCC 到点 → 依次唤起,防模型并发挤兑)
|
|
3613
|
+
*
|
|
3614
|
+
* 零资源占用语义保留:无 enabled CCC → tick 内直接 return(定时器存在但每 10min 一次空检查,
|
|
3615
|
+
* unref 不阻塞进程退出)。
|
|
3616
|
+
*/
|
|
3617
|
+
function registerAutopilot(ctx) {
|
|
3618
|
+
let timer = null;
|
|
3619
|
+
const runningByRoot = /* @__PURE__ */ new Map();
|
|
3620
|
+
let wakeChain = Promise.resolve();
|
|
3621
|
+
const globalOn = () => {
|
|
3622
|
+
try {
|
|
3623
|
+
return readSimpleSettings().autopilotEnabled === true;
|
|
3624
|
+
} catch {
|
|
3625
|
+
return false;
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
const tick = () => {
|
|
3629
|
+
if (!globalOn()) return;
|
|
3630
|
+
const roots = collectAutopilotCccs(ctx);
|
|
3631
|
+
if (roots.length === 0) return;
|
|
3632
|
+
for (const root of roots) {
|
|
3633
|
+
if (runningByRoot.get(root)) continue;
|
|
3634
|
+
const settings = readAutopilotSettings(root);
|
|
3635
|
+
if (!settings?.enabled) continue;
|
|
3636
|
+
if (!shouldWake(settings, resolveTargetMd(root, settings), Date.now(), false, wakeHistoryFor(root))) continue;
|
|
3637
|
+
runningByRoot.set(root, true);
|
|
3638
|
+
wakeChain = wakeChain.then(async () => {
|
|
3639
|
+
try {
|
|
3640
|
+
const res = await performAutopilotWake(ctx, root, settings, { force: false });
|
|
3641
|
+
recordWake(root, {
|
|
3642
|
+
time: Date.now(),
|
|
3643
|
+
ok: res.ok,
|
|
3644
|
+
detail: res.detail
|
|
3645
|
+
});
|
|
3646
|
+
if (res.ok) console.log(`[serenity-hooks] ✓ Autopilot Trajectory 唤起(${res.detail})`);
|
|
3647
|
+
else console.warn(`[serenity-hooks] ✗ Autopilot Trajectory 唤起跳过:${res.detail}`);
|
|
3648
|
+
} catch (err) {
|
|
3649
|
+
recordWake(root, {
|
|
3650
|
+
time: Date.now(),
|
|
3651
|
+
ok: false,
|
|
3652
|
+
detail: `唤起异常: ${String(err?.message ?? err)}`
|
|
3653
|
+
});
|
|
3654
|
+
console.warn(`[serenity-hooks] ✗ Autopilot Trajectory 唤起失败: ${String(err?.message ?? err)}`);
|
|
3655
|
+
} finally {
|
|
3656
|
+
runningByRoot.set(root, false);
|
|
3657
|
+
}
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
const startTimer = () => {
|
|
3662
|
+
if (timer) return;
|
|
3663
|
+
if (!globalOn()) return;
|
|
3664
|
+
if (collectAutopilotCccs(ctx).length === 0) return;
|
|
3665
|
+
timer = setInterval(tick, TICK_MS);
|
|
3666
|
+
timer.unref();
|
|
3667
|
+
console.log(`[serenity-hooks] ✓ Autopilot Trajectory 定时器启动(${collectAutopilotCccs(ctx).length} 个 CCC 启用)`);
|
|
3668
|
+
tick();
|
|
3669
|
+
};
|
|
3670
|
+
startTimer();
|
|
3671
|
+
try {
|
|
3672
|
+
ctx.on("session/created", () => startTimer());
|
|
3673
|
+
} catch {}
|
|
3674
|
+
try {
|
|
3675
|
+
ctx.on("serenity/settings-changed", () => startTimer());
|
|
3676
|
+
} catch {}
|
|
3677
|
+
}
|
|
3678
|
+
/**
|
|
3679
|
+
* 目标 agent:从 SESSION.md 反向定位 dsh 会话(cwd 归属校验 + **bound 精确匹配优先**,
|
|
3680
|
+
* 标题 F3 命名回退);不可得 → null。
|
|
3681
|
+
*
|
|
3682
|
+
* v1.29.2(R2,用户"autopilot 会话绑定应当更稳固——唤起的时候能唤起最新的、
|
|
3683
|
+
* 绑定 autopilot SESSION 的会话"):v1.29.1 `serenity/bound` 事件是权威绑定
|
|
3684
|
+
* (session use 激活时 append,dirName = 完整 AGENT_SESSIONS 目录名,编码无关 U4)。
|
|
3685
|
+
* 旧实现只按标题猜(=== sid / startsWith(sid-))——若绑定该 SESSION 的 dsh 会话
|
|
3686
|
+
* 标题不是 S###- 前缀(LLM 改过/重建后 rename 异常),绑定明明在却唤起失败。
|
|
3687
|
+
* 加强:**bound.dirName 精确匹配优先**(权威证据),标题匹配降级为回退(存量
|
|
3688
|
+
* 无 bound 的 live 会话兼容);多候选(罕见:同 SESSION 绑定多个 live 会话)取
|
|
3689
|
+
* **绑定最新**(bound.at 最大——最近 use 过 = 最可能当前在用)。
|
|
3690
|
+
*/
|
|
3691
|
+
function resolveTargetAgent(ctx, mdPath) {
|
|
3692
|
+
const dirName = basename(dirname(mdPath));
|
|
3693
|
+
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
3694
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
3695
|
+
const targetRoot = findSerenityRoot(mdPath);
|
|
3696
|
+
const sessions = ctx.sessions;
|
|
3697
|
+
const agents = ctx.agents;
|
|
3698
|
+
const candidates = [];
|
|
3699
|
+
try {
|
|
3700
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
3701
|
+
const sess = s;
|
|
3702
|
+
const cwd = sess?.header?.cwd ?? "";
|
|
3703
|
+
if (targetRoot) {
|
|
3704
|
+
if (findSerenityRoot(cwd) !== targetRoot) continue;
|
|
3705
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
3706
|
+
const bound = readLastBound(sess);
|
|
3707
|
+
const title = readSessionTitle(sess);
|
|
3708
|
+
const boundMatched = bound !== null && bound.dirName === dirName;
|
|
3709
|
+
const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
|
|
3710
|
+
if (boundMatched) candidates.push({
|
|
3711
|
+
session: sess,
|
|
3712
|
+
boundAt: bound?.at ?? null,
|
|
3713
|
+
titleMatched
|
|
3714
|
+
});
|
|
3715
|
+
else if (titleMatched) candidates.push({
|
|
3716
|
+
session: sess,
|
|
3717
|
+
boundAt: null,
|
|
3718
|
+
titleMatched
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
} catch {}
|
|
3722
|
+
if (candidates.length === 0) return null;
|
|
3723
|
+
candidates.sort((a, b) => {
|
|
3724
|
+
const aBound = a.boundAt !== null;
|
|
3725
|
+
if (aBound !== (b.boundAt !== null)) return aBound ? -1 : 1;
|
|
3726
|
+
return (b.boundAt ?? 0) - (a.boundAt ?? 0);
|
|
3727
|
+
});
|
|
3728
|
+
for (const c of candidates) {
|
|
3729
|
+
const agent = agents?.get?.(c.session.id ?? "");
|
|
3730
|
+
if (agent) return agent;
|
|
3731
|
+
}
|
|
3732
|
+
return null;
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3735
|
+
* 从 dsh 会话 log 读取标题(latest-wins `session/title` 事件)——
|
|
3736
|
+
* **标题不在 sessions.list() 条目上**(wire/对象均无 title 字段;F3 命名经
|
|
3737
|
+
* sessionTitle.rename 写进 session log),必须从 events 提取(rebuild 同款读取模式)。
|
|
3738
|
+
*/
|
|
3739
|
+
function readSessionTitle(session) {
|
|
3740
|
+
try {
|
|
3741
|
+
const events = sessionEvents(session);
|
|
3742
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
3743
|
+
const e = events[i];
|
|
3744
|
+
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|
|
3745
|
+
}
|
|
3746
|
+
} catch {}
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* 诊断:目标会话 agent 为何不可得(供 performAutopilotWake 失败信息——
|
|
3751
|
+
* 区分"无 live 会话" / "标题不匹配" / "bound 或标题命中但 agent 未加载")。
|
|
3752
|
+
* v1.29.2(R2):bound 命中但 agent 未加载也归入「已匹配未加载」(原只查标题)。
|
|
3753
|
+
* 返回诊断文本(无则 null)。
|
|
3754
|
+
*/
|
|
3755
|
+
function diagnoseTargetUnavailable(ctx, mdPath) {
|
|
3756
|
+
const dirName = basename(dirname(mdPath));
|
|
3757
|
+
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
3758
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
3759
|
+
const targetRoot = findSerenityRoot(mdPath);
|
|
3760
|
+
const sameCccTitles = [];
|
|
3761
|
+
const agentMissing = { matched: false };
|
|
3762
|
+
try {
|
|
3763
|
+
const sessions = ctx.sessions;
|
|
3764
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
3765
|
+
const sess = s;
|
|
3766
|
+
const cwd = sess?.header?.cwd ?? "";
|
|
3767
|
+
if (targetRoot) {
|
|
3768
|
+
if (findSerenityRoot(cwd) !== targetRoot) continue;
|
|
3769
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
3770
|
+
const bound = readLastBound(sess);
|
|
3771
|
+
const title = readSessionTitle(sess);
|
|
3772
|
+
if (title) sameCccTitles.push(title);
|
|
3773
|
+
const boundMatched = bound !== null && bound.dirName === dirName;
|
|
3774
|
+
const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
|
|
3775
|
+
if (boundMatched || titleMatched) {
|
|
3776
|
+
if (!ctx.agents?.get?.(sess.id ?? "")) agentMissing.matched = true;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
} catch {}
|
|
3780
|
+
if (agentMissing.matched) return `会话已绑定/匹配 ${sid ?? dirName} 但 agent 未加载(会话可能刚创建/正在恢复——稍后重试)`;
|
|
3781
|
+
if (sameCccTitles.length > 0) return `目标 CCC 内 live 会话标题: [${sameCccTitles.join(", ")}]——均不匹配 ${sid ?? dirName}(目标会话未在 WebUI 打开,或绑定/命名未生效)`;
|
|
3782
|
+
return `目标 CCC 内无 live 会话(先在 WebUI 打开 ${sid ?? dirName} 会话后重试)`;
|
|
3783
|
+
}
|
|
3784
|
+
/** Autopilot 绑定的回退 CCC 根:进程 cwd 上溯 .serenity 优先,回退任一 live 会话 root */
|
|
3785
|
+
function resolveAutopilotRoot(ctx) {
|
|
3786
|
+
const fromCwd = findSerenityRoot(process.cwd());
|
|
3787
|
+
if (fromCwd) return fromCwd;
|
|
3788
|
+
try {
|
|
3789
|
+
const sessions = ctx.sessions;
|
|
3790
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
3791
|
+
const r = findSerenityRoot(s?.header?.cwd ?? "");
|
|
3792
|
+
if (r) return r;
|
|
3793
|
+
}
|
|
3794
|
+
} catch {}
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
function getAutopilotStatus(root) {
|
|
3798
|
+
const cfg = readAutopilotSettings(root);
|
|
3799
|
+
const now = Date.now();
|
|
3800
|
+
const base = {
|
|
3801
|
+
configured: cfg !== null,
|
|
3802
|
+
enabled: cfg?.enabled ?? false,
|
|
3803
|
+
intervalHours: Math.max(MIN_INTERVAL_HOURS, cfg?.intervalHours ?? 12),
|
|
3804
|
+
biasProvider: cfg?.biasProvider?.trim() || "autopilot-bias.ts",
|
|
3805
|
+
topPrompt: cfg?.topPrompt?.trim() || null,
|
|
3806
|
+
session: cfg?.session ?? null,
|
|
3807
|
+
avoidWakeHours: {
|
|
3808
|
+
start: cfg?.avoidWakeHours?.start ?? DEFAULT_AVOID_HOURS.start,
|
|
3809
|
+
end: cfg?.avoidWakeHours?.end ?? DEFAULT_AVOID_HOURS.end
|
|
3810
|
+
},
|
|
3811
|
+
beijingHour: beijingHour(now),
|
|
3812
|
+
windowAllowed: inAllowedWakeWindow(now, cfg?.avoidWakeHours),
|
|
3813
|
+
recentWakes: wakeHistoryFor(root).slice(-10).reverse()
|
|
3814
|
+
};
|
|
3815
|
+
if (!cfg) return {
|
|
3816
|
+
...base,
|
|
3817
|
+
target: null
|
|
3818
|
+
};
|
|
3819
|
+
const mdPath = resolveTargetMd(root, cfg);
|
|
3820
|
+
if (!mdPath) return {
|
|
3821
|
+
...base,
|
|
3822
|
+
target: null
|
|
3823
|
+
};
|
|
3824
|
+
let idleHours = 0;
|
|
3825
|
+
try {
|
|
3826
|
+
idleHours = (now - statSync(mdPath).mtimeMs) / 36e5;
|
|
3827
|
+
} catch {}
|
|
3828
|
+
return {
|
|
3829
|
+
...base,
|
|
3830
|
+
target: {
|
|
3831
|
+
dirName: basename(dirname(mdPath)),
|
|
3832
|
+
autoFlag: isAutopilotSession(mdPath),
|
|
3833
|
+
idleHours: Math.max(0, idleHours),
|
|
3834
|
+
wakeable: shouldWake(cfg, mdPath, now, false, wakeHistoryFor(root))
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
/** 遍历 live 会话(sessions.list())+ 补标题(events session/title latest-wins)+ ccc 归属 */
|
|
3839
|
+
function listLiveSessions(ctx) {
|
|
3840
|
+
const out = [];
|
|
3841
|
+
try {
|
|
3842
|
+
const sessions = ctx.sessions;
|
|
3843
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
3844
|
+
const cwd = s?.header?.cwd ?? null;
|
|
3845
|
+
out.push({
|
|
3846
|
+
id: s?.id ?? "",
|
|
3847
|
+
cwd,
|
|
3848
|
+
cccRoot: cwd ? findSerenityRoot(cwd) : null,
|
|
3849
|
+
title: readSessionTitle(s)
|
|
3850
|
+
});
|
|
3851
|
+
}
|
|
3852
|
+
} catch {}
|
|
3853
|
+
return out;
|
|
3854
|
+
}
|
|
3855
|
+
/**
|
|
3856
|
+
* 收集所有「配置了 Autopilot Trajectory 的 live CCC」——**v1.27.4 多 CCC 独立**:
|
|
3857
|
+
* 返回 enabled 的 CCC 列表(时钟 tick 遍历目标);无 enabled → []。
|
|
3858
|
+
* (原 resolveAutoTrajectoryCcc 单目标语义被 collect 取代——面板/诊断用 enableOnly=false 取全量)
|
|
3859
|
+
*/
|
|
3860
|
+
function collectAutopilotCccs(ctx, opts = {}) {
|
|
3861
|
+
const enabled = [];
|
|
3862
|
+
const configured = [];
|
|
3863
|
+
for (const s of listLiveSessions(ctx)) {
|
|
3864
|
+
if (!s.cccRoot) continue;
|
|
3865
|
+
const cfg = readAutopilotSettings(s.cccRoot);
|
|
3866
|
+
if (!cfg) continue;
|
|
3867
|
+
if (cfg.enabled) {
|
|
3868
|
+
if (!enabled.includes(s.cccRoot)) enabled.push(s.cccRoot);
|
|
3869
|
+
} else if (!configured.includes(s.cccRoot)) configured.push(s.cccRoot);
|
|
3870
|
+
}
|
|
3871
|
+
return opts.includeDisabled ? [...enabled, ...configured] : enabled;
|
|
3872
|
+
}
|
|
3873
|
+
function diagLive(ctx) {
|
|
3874
|
+
const liveSessions = listLiveSessions(ctx);
|
|
3875
|
+
const processCwd = process.cwd();
|
|
3876
|
+
const processCcc = findSerenityRoot(processCwd);
|
|
3877
|
+
const panelResolved = collectAutopilotCccs(ctx)[0] ?? resolveAutopilotRoot(ctx);
|
|
3878
|
+
const autopilotCccs = [];
|
|
3879
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3880
|
+
for (const s of liveSessions) {
|
|
3881
|
+
if (!s.cccRoot || seen.has(s.cccRoot)) continue;
|
|
3882
|
+
const cfg = readAutopilotSettings(s.cccRoot);
|
|
3883
|
+
if (!cfg) continue;
|
|
3884
|
+
seen.add(s.cccRoot);
|
|
3885
|
+
const status = getAutopilotStatus(s.cccRoot);
|
|
3886
|
+
const mdPath = resolveTargetMd(s.cccRoot, cfg);
|
|
3887
|
+
const agentResolved = mdPath ? resolveTargetAgent(ctx, mdPath) !== null : false;
|
|
3888
|
+
const agentDiagnosis = mdPath && !agentResolved ? diagnoseTargetUnavailable(ctx, mdPath) : null;
|
|
3889
|
+
autopilotCccs.push({
|
|
3890
|
+
root: s.cccRoot,
|
|
3891
|
+
enabled: status.enabled,
|
|
3892
|
+
session: status.session,
|
|
3893
|
+
target: status.target,
|
|
3894
|
+
agentResolved,
|
|
3895
|
+
agentDiagnosis
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
return {
|
|
3899
|
+
processCwd,
|
|
3900
|
+
processCcc,
|
|
3901
|
+
liveSessions,
|
|
3902
|
+
autopilotCccs,
|
|
3903
|
+
panelResolved
|
|
4876
3904
|
};
|
|
4877
3905
|
}
|
|
3906
|
+
//#endregion
|
|
3907
|
+
//#region src/tools/autopilot-trajectory.ts
|
|
4878
3908
|
/**
|
|
4879
|
-
*
|
|
4880
|
-
*
|
|
4881
|
-
* v1.26.14 根因:旧实现启动时一次性解析 root——web 进程启动时 live 会话往往为空 → 定时器不启动。
|
|
4882
|
-
* 修复:① 每次 tick 动态解析 root+settings ② 监听 session/created 启动定时器 ③ 优先实验 CCC。
|
|
3909
|
+
* autopilot-trajectory.ts — Autopilot Trajectory 一站式管理工具(v1.26.12 实验
|
|
3910
|
+
* autotrajectory-exp → v1.27.4 正式化改名)
|
|
4883
3911
|
*
|
|
4884
|
-
*
|
|
4885
|
-
*
|
|
4886
|
-
*
|
|
4887
|
-
* - `running` 守卫 **per-CCC**(不同 CCC 唤起互不阻塞)
|
|
4888
|
-
* - **全局串行化**(wakeChain:同 tick 多 CCC 到点 → 依次唤起,防模型并发挤兑)
|
|
3912
|
+
* 定位:dsp **只提供工具与知识**,不向 CCC 自动安装任何东西(机制是 CCC 的自选动作)——
|
|
3913
|
+
* agent 调本工具(doc/全报告)即懂机制,init/random/check 辅助,
|
|
3914
|
+
* 实际执行(写配置/写偏见脚本/标记会话)由 CCC 自己决定、自己用现有工具完成。
|
|
4889
3915
|
*
|
|
4890
|
-
*
|
|
4891
|
-
*
|
|
3916
|
+
* 实现:薄封装——exec 包内静态脚本(npm files 含 experiments/),脚本是单一真相源。
|
|
3917
|
+
* 环境注入 SERENITY_ROOT(当前 CCC 根)供脚本定位;bun 优先(可直跑 TS)。
|
|
3918
|
+
* diag-live:**进程内诊断**(v1.26.14 用户"排查访问不到")——闭包捕获 ctx,
|
|
3919
|
+
* 直接读 live 会话/标题/agent(脚本 diag 看不到运行时),输出实例级诊断报告。
|
|
4892
3920
|
*/
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
if (
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
for (const root of roots) {
|
|
4909
|
-
if (runningByRoot.get(root)) continue;
|
|
4910
|
-
const settings = readAutopilotSettings(root);
|
|
4911
|
-
if (!settings?.enabled) continue;
|
|
4912
|
-
if (!shouldWake(settings, resolveTargetMd(root, settings), Date.now(), false, wakeHistoryFor(root))) continue;
|
|
4913
|
-
runningByRoot.set(root, true);
|
|
4914
|
-
wakeChain = wakeChain.then(async () => {
|
|
4915
|
-
try {
|
|
4916
|
-
const res = await performAutopilotWake(ctx, root, settings, { force: false });
|
|
4917
|
-
recordWake(root, {
|
|
4918
|
-
time: Date.now(),
|
|
4919
|
-
ok: res.ok,
|
|
4920
|
-
detail: res.detail
|
|
4921
|
-
});
|
|
4922
|
-
if (res.ok) console.log(`[serenity-hooks] ✓ Autopilot Trajectory 唤起(${res.detail})`);
|
|
4923
|
-
else console.warn(`[serenity-hooks] ✗ Autopilot Trajectory 唤起跳过:${res.detail}`);
|
|
4924
|
-
} catch (err) {
|
|
4925
|
-
recordWake(root, {
|
|
4926
|
-
time: Date.now(),
|
|
4927
|
-
ok: false,
|
|
4928
|
-
detail: `唤起异常: ${String(err?.message ?? err)}`
|
|
4929
|
-
});
|
|
4930
|
-
console.warn(`[serenity-hooks] ✗ Autopilot Trajectory 唤起失败: ${String(err?.message ?? err)}`);
|
|
4931
|
-
} finally {
|
|
4932
|
-
runningByRoot.set(root, false);
|
|
4933
|
-
}
|
|
4934
|
-
});
|
|
4935
|
-
}
|
|
4936
|
-
};
|
|
4937
|
-
const startTimer = () => {
|
|
4938
|
-
if (timer) return;
|
|
4939
|
-
if (!globalOn()) return;
|
|
4940
|
-
if (collectAutopilotCccs(ctx).length === 0) return;
|
|
4941
|
-
timer = setInterval(tick, TICK_MS);
|
|
4942
|
-
timer.unref();
|
|
4943
|
-
console.log(`[serenity-hooks] ✓ Autopilot Trajectory 定时器启动(${collectAutopilotCccs(ctx).length} 个 CCC 启用)`);
|
|
4944
|
-
tick();
|
|
4945
|
-
};
|
|
4946
|
-
startTimer();
|
|
4947
|
-
try {
|
|
4948
|
-
ctx.on("session/created", () => startTimer());
|
|
4949
|
-
} catch {}
|
|
4950
|
-
try {
|
|
4951
|
-
ctx.on("serenity/settings-changed", () => startTimer());
|
|
4952
|
-
} catch {}
|
|
3921
|
+
/**
|
|
3922
|
+
* 定位包内脚本(npm files 分发 experiments/autopilot-trajectory/)。
|
|
3923
|
+
* 布局差异:tsdown bundle 后 import.meta.url 指向 lib/index.js(lib → 包根 1 层);
|
|
3924
|
+
* vitest 源码直跑时指向 src/tools/x.ts(src/tools → 包根 2 层)——逐级上溯查找,
|
|
3925
|
+
* 两种布局都稳(找到 experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts 即止)。
|
|
3926
|
+
*/
|
|
3927
|
+
function findExpScript(startDir) {
|
|
3928
|
+
let cur = startDir;
|
|
3929
|
+
while (true) {
|
|
3930
|
+
const cand = join(cur, "experiments", "autopilot-trajectory", "scripts", "autopilot-trajectory.ts");
|
|
3931
|
+
if (existsSync(cand)) return cand;
|
|
3932
|
+
const parent = dirname(cur);
|
|
3933
|
+
if (parent === cur) return null;
|
|
3934
|
+
cur = parent;
|
|
3935
|
+
}
|
|
4953
3936
|
}
|
|
4954
|
-
/**
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
3937
|
+
/** 包内脚本(上溯查找;找不到 → execute 报错提示包完整性) */
|
|
3938
|
+
const EXP_SCRIPT = findExpScript(dirname(fileURLToPath(import.meta.url)));
|
|
3939
|
+
const AUTOPILOT_ACTIONS = [
|
|
3940
|
+
"all",
|
|
3941
|
+
"init",
|
|
3942
|
+
"random",
|
|
3943
|
+
"diag",
|
|
3944
|
+
"doc",
|
|
3945
|
+
"check",
|
|
3946
|
+
"status",
|
|
3947
|
+
"guide",
|
|
3948
|
+
"diag-live"
|
|
3949
|
+
];
|
|
3950
|
+
function agentCwd$1(exec) {
|
|
3951
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
3952
|
+
}
|
|
3953
|
+
function renderText(value) {
|
|
3954
|
+
return [{
|
|
3955
|
+
type: "text",
|
|
3956
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
3957
|
+
}];
|
|
3958
|
+
}
|
|
3959
|
+
/** 渲染 diag-live 报告为可读文本(EAP:结构清晰、命中点显式) */
|
|
3960
|
+
function renderDiagLive(r) {
|
|
3961
|
+
const lines = [
|
|
3962
|
+
"═══ Autopilot Trajectory 进程内诊断(diag-live,v1.26.14)═══",
|
|
3963
|
+
`进程 cwd: ${r.processCwd}(CCC: ${r.processCcc ?? "无 .serenity"})`,
|
|
3964
|
+
`面板解析(无参 GET 目标): ${r.panelResolved ?? "无配置 Autopilot 的 live CCC"}`,
|
|
3965
|
+
"",
|
|
3966
|
+
`── live 会话(${r.liveSessions.length})──`
|
|
3967
|
+
];
|
|
3968
|
+
if (r.liveSessions.length === 0) lines.push(" (无 live 会话——WebUI 未打开任何会话;定时器绑定依赖 live 会话 cwd)");
|
|
3969
|
+
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}` : ""}`);
|
|
3970
|
+
lines.push("", `── 配置了 Autopilot Trajectory 的 CCC(${r.autopilotCccs.length})──`);
|
|
3971
|
+
if (r.autopilotCccs.length === 0) lines.push(" (无——live 会话中无任何 CCC 配置 autopilotTrajectory)");
|
|
3972
|
+
for (const c of r.autopilotCccs) {
|
|
3973
|
+
lines.push(` · ${c.root}(enabled=${c.enabled})`);
|
|
3974
|
+
lines.push(` session=${c.session ?? "未配置"} | 目标=${c.target ? `${c.target.dirName}(--auto ${c.target.autoFlag ? "✓" : "✗"},空闲 ${c.target.idleHours.toFixed(1)}h)` : "未命中"}`);
|
|
3975
|
+
lines.push(` agent 定位: ${c.agentResolved ? "✓ 可注入" : `✗ 不可得——${c.agentDiagnosis ?? "未知"}`}`);
|
|
3976
|
+
}
|
|
3977
|
+
return lines.join("\n");
|
|
3978
|
+
}
|
|
3979
|
+
/** 创建 autopilot-trajectory 工具(闭包捕获 ctx → diag-live 进程内诊断;v1.26.14 + v1.27.4 改名) */
|
|
3980
|
+
function createAutopilotTool(ctx) {
|
|
3981
|
+
return defineTool({
|
|
3982
|
+
name: "autopilot-trajectory",
|
|
3983
|
+
description: "Autopilot Trajectory(自动巡航轨迹,正式版 v1.27.4;前身 autotrajectory 实验)一站式管理。无参/action=all:全报告(背景摘要 + 就绪检查 + 状态 + 下一步)——CCC agent 看一次即完整理解并知道怎么开始;init:初始化辅助(写配置 + 生成偏见提供者脚本模板);random:运行偏见提供者脚本输出当前偏见内容;diag:唤起条件链诊断(--ccc <path> 指定 CCC,无参递归扫描 /home/yh 两层;逐条件输出 + 阻断点 + 修复建议);diag-live:**进程内诊断**(live 会话清单/标题/agent 定位/面板解析目标;排查\"面板检测不到实验 CCC\");doc:定义全文;check/status/guide:单项。**topPrompt(轨迹焦点)**:CCC 定义时自己填写本轨迹核心焦点(顶层提示词),每次唤起最先注入——稳定焦点锚定防漂移,与偏见内容(每轮随机探索)互补。多 CCC 独立(v1.27.4):每个 live+enabled CCC 各自时钟唤起。机制是 CCC 的自选动作——dsp 只提供工具与知识,不自动安装任何东西。",
|
|
3984
|
+
parameters: { action: {
|
|
3985
|
+
type: "string",
|
|
3986
|
+
enum: [...AUTOPILOT_ACTIONS],
|
|
3987
|
+
required: true,
|
|
3988
|
+
description: "Subcommand"
|
|
3989
|
+
} },
|
|
3990
|
+
output: {
|
|
3991
|
+
schema: { type: "json" },
|
|
3992
|
+
render: (_args, value) => renderText(value)
|
|
3993
|
+
},
|
|
3994
|
+
async execute(args, exec) {
|
|
3995
|
+
if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
|
|
3996
|
+
const root = findSerenityRoot(agentCwd$1(exec));
|
|
3997
|
+
const result = {};
|
|
3998
|
+
if (!EXP_SCRIPT) {
|
|
3999
|
+
result.error = "autopilot-trajectory 脚本未随安装分发(npm 包缺 experiments/autopilot-trajectory/)——请检查包完整性";
|
|
4000
|
+
return result;
|
|
4970
4001
|
}
|
|
4002
|
+
const script = EXP_SCRIPT;
|
|
4003
|
+
if (!existsSync(script)) {
|
|
4004
|
+
result.error = `autopilot-trajectory 脚本缺失(${script})——包未随安装分发,请检查 npm 包完整性`;
|
|
4005
|
+
return result;
|
|
4006
|
+
}
|
|
4007
|
+
const env = { ...process.env };
|
|
4008
|
+
if (root) env.SERENITY_ROOT = root;
|
|
4009
|
+
const r = spawnSync("bun", [script, args.action ?? "all"], {
|
|
4010
|
+
encoding: "utf-8",
|
|
4011
|
+
timeout: 6e5,
|
|
4012
|
+
env,
|
|
4013
|
+
stdio: [
|
|
4014
|
+
"ignore",
|
|
4015
|
+
"pipe",
|
|
4016
|
+
"pipe"
|
|
4017
|
+
]
|
|
4018
|
+
});
|
|
4019
|
+
if (r.status === 0) {
|
|
4020
|
+
result.output = r.stdout?.trim() || "(empty)";
|
|
4021
|
+
return result;
|
|
4022
|
+
}
|
|
4023
|
+
if (r.error?.code === "ENOENT") {
|
|
4024
|
+
result.error = "autopilot-trajectory 需要 bun 运行时(bun not found in PATH)";
|
|
4025
|
+
return result;
|
|
4026
|
+
}
|
|
4027
|
+
result.error = r.stderr?.trim() || r.stdout?.trim() || `exit ${r.status ?? "?"}`;
|
|
4028
|
+
return result;
|
|
4971
4029
|
}
|
|
4972
|
-
}
|
|
4973
|
-
return null;
|
|
4030
|
+
});
|
|
4974
4031
|
}
|
|
4032
|
+
//#endregion
|
|
4033
|
+
//#region src/seams/bootstrap.ts
|
|
4034
|
+
/** 默认首请求工具集:dsp 平台 Minimal 等价核心(anchored 是 bash+str_replace_editor) */
|
|
4035
|
+
const DEFAULT_BOOTSTRAP_TOOLS = [
|
|
4036
|
+
"read",
|
|
4037
|
+
"write",
|
|
4038
|
+
"edit",
|
|
4039
|
+
"glob",
|
|
4040
|
+
"grep"
|
|
4041
|
+
];
|
|
4042
|
+
/** 默认剥离的自动注入源(anchored 默认:可用技能目录提醒 + 工作区指令摘要) */
|
|
4043
|
+
const DEFAULT_SUPPRESSED_SOURCES = ["skill-catalog", "agent-instructions"];
|
|
4044
|
+
/** 默认 compaction 恢复工作集(anchored 默认 read/write/edit/glob/grep/todo_write/ask_user_question) */
|
|
4045
|
+
const DEFAULT_COMPACTION_TOOLS = [
|
|
4046
|
+
"read",
|
|
4047
|
+
"write",
|
|
4048
|
+
"edit",
|
|
4049
|
+
"glob",
|
|
4050
|
+
"grep",
|
|
4051
|
+
"todo_write"
|
|
4052
|
+
];
|
|
4975
4053
|
/**
|
|
4976
|
-
*
|
|
4977
|
-
*
|
|
4978
|
-
*
|
|
4054
|
+
* 协议级锚定消息序列(用户确认固化,2026-08-24 S142):任何 CCC 在抽象层都是
|
|
4055
|
+
* 宁静号/ACC——首轮锚定话语统一,无 CCC 配置面。两轮递进:
|
|
4056
|
+
* ① ACC 身份 + EAP 原则 + we/us 人称 + 先锚定后行动
|
|
4057
|
+
* ② 协作协议 5 条 + acknowledge 要求(只回确认,不做事)
|
|
4058
|
+
* 文本 = 原 home-serenity CCC 配置(anchorMessages),随零配置化固化进代码。
|
|
4059
|
+
*/
|
|
4060
|
+
const DEFAULT_ANCHOR_MESSAGES = ["You are the operator agent of Serenity — a cognitive container governed by the Abstract Cognitive Container (ACC) protocol.\nWe operate under the Explicit Abstraction Principle (EAP): the functional value of a thought equals its external reconstructability — every output we produce must be explicit (E↑), reconstructable (R↓), and stable (S↑).\nThe personal pronoun is us/we.\nWe anchor first, then act: the abstract layer precedes the concrete.\nPlease simply reply \"acknowledge\" — no action needed.", "Before we proceed, align on how we work:\n1. We read before we write — every decision grounds in what already exists in the container.\n2. Every output records its reasoning (R↓): decisions carry reasons and alternatives.\n3. We never jump levels — abstract layer first, then specifics.\n4. Every artifact we create is a durable cognitive anchor for the work that follows.\n5. We keep the container's state coherent (SESSION.md) as we advance.\nPlease simply reply \"acknowledge\" — no action needed."];
|
|
4061
|
+
/**
|
|
4062
|
+
* 根会话锚定重入判定(v1.28.1 提取导出以便单测):会话是否已有真实用户消息。
|
|
4063
|
+
* resume/续跑的会话已有对话历史 → **不重锚**(first-anchor 只注入一次)。
|
|
4064
|
+
*
|
|
4065
|
+
* 0.1.2-rc.1 适配教训:裸读 `session.events`(经类型断言)恒 undefined →
|
|
4066
|
+
* `!undefined` 恒 true → 有历史也永不跳过 → 任何情况发消息都重插 first-anchor。
|
|
4067
|
+
* 统一经 sessionEvents() 读取(snapshotEvents() 优先,.events 兜底)。
|
|
4979
4068
|
*/
|
|
4980
|
-
function
|
|
4981
|
-
|
|
4982
|
-
const events = sessionEvents(session);
|
|
4983
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
4984
|
-
const e = events[i];
|
|
4985
|
-
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|
|
4986
|
-
}
|
|
4987
|
-
} catch {}
|
|
4988
|
-
return null;
|
|
4069
|
+
function hasUserMessageHistory(session) {
|
|
4070
|
+
return sessionEvents(session).some((event) => event.type === "user/message");
|
|
4989
4071
|
}
|
|
4990
4072
|
/**
|
|
4991
|
-
*
|
|
4992
|
-
*
|
|
4073
|
+
* 构建一个 epoch 感知晋升跟踪器(纯逻辑,可单测)。
|
|
4074
|
+
* requiredSignals:晋升所需信号数(boundary 后累计;默认 1)。
|
|
4075
|
+
* 多轮锚定(v4):两轮递进锚定时 requiredSignals = 锚定轮数——
|
|
4076
|
+
* 每条锚定回复(assistant/message)计一次,最后一条回复后晋升。
|
|
4077
|
+
*
|
|
4078
|
+
* maxRoundsFallback:**轮次兜底**(v1.19.3 修复 responses API 兼容)。
|
|
4079
|
+
* 某些模型/协议(如 opencode-go-responses)的会话可能不产生标准
|
|
4080
|
+
* `assistant/message` 晋升信号 → 永不晋升 → bootstrap 阶段工具被裁空
|
|
4081
|
+
* (zeroTools 首请求 0 工具,ACC 工具不可见)。兜底:无论晋升信号是否
|
|
4082
|
+
* 到达,观察到的模型 step 数(`step/start` 事件计数,平台稳定事件,
|
|
4083
|
+
* responses/completions 都触发,独立于 promoteEvents)达到 maxRoundsFallback
|
|
4084
|
+
* 即强制 promoted(开放完整工具)。正常模型锚定轮数(requiredSignals)
|
|
4085
|
+
* 通常 ≤ 2 < 默认兜底 3,不受影响。
|
|
4993
4086
|
*/
|
|
4994
|
-
function
|
|
4995
|
-
|
|
4996
|
-
const
|
|
4997
|
-
const
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
const sessions = ctx.sessions;
|
|
5002
|
-
for (const s of sessions?.list?.() ?? []) {
|
|
5003
|
-
const cwd = s?.header?.cwd ?? "";
|
|
5004
|
-
if (targetRoot) {
|
|
5005
|
-
if (findSerenityRoot(cwd) !== targetRoot) continue;
|
|
5006
|
-
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
5007
|
-
const title = readSessionTitle(s);
|
|
5008
|
-
if (title) sameCccTitles.push(title);
|
|
5009
|
-
if (sid && title && (title === sid || title.startsWith(`${sid}-`))) {
|
|
5010
|
-
if (!ctx.agents?.get?.(s.id ?? "")) agentMissing.sid = true;
|
|
5011
|
-
}
|
|
5012
|
-
}
|
|
5013
|
-
} catch {}
|
|
5014
|
-
if (agentMissing.sid) return `会话 ${sid} 已打开但 agent 未加载(会话可能刚创建/正在恢复——稍后重试)`;
|
|
5015
|
-
if (sameCccTitles.length > 0) return `目标 CCC 内 live 会话标题: [${sameCccTitles.join(", ")}]——均不匹配 ${sid}(目标会话未在 WebUI 打开,或命名未生效)`;
|
|
5016
|
-
return `目标 CCC 内无 live 会话(先在 WebUI 打开 ${sid} 会话后重试)`;
|
|
5017
|
-
}
|
|
5018
|
-
/** Autopilot 绑定的回退 CCC 根:进程 cwd 上溯 .serenity 优先,回退任一 live 会话 root */
|
|
5019
|
-
function resolveAutopilotRoot(ctx) {
|
|
5020
|
-
const fromCwd = findSerenityRoot(process.cwd());
|
|
5021
|
-
if (fromCwd) return fromCwd;
|
|
5022
|
-
try {
|
|
5023
|
-
const sessions = ctx.sessions;
|
|
5024
|
-
for (const s of sessions?.list?.() ?? []) {
|
|
5025
|
-
const r = findSerenityRoot(s?.header?.cwd ?? "");
|
|
5026
|
-
if (r) return r;
|
|
4087
|
+
function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallback = 3) {
|
|
4088
|
+
/** sessionId -> { boundary, signalCount, rounds }(rounds = 兜底用回复轮计数) */
|
|
4089
|
+
const state = /* @__PURE__ */ new Map();
|
|
4090
|
+
const sessionIdOf = (session) => {
|
|
4091
|
+
if (session && typeof session === "object") {
|
|
4092
|
+
const id = session.id;
|
|
4093
|
+
if (typeof id === "string") return id;
|
|
5027
4094
|
}
|
|
5028
|
-
} catch {}
|
|
5029
|
-
return null;
|
|
5030
|
-
}
|
|
5031
|
-
function getAutopilotStatus(root) {
|
|
5032
|
-
const cfg = readAutopilotSettings(root);
|
|
5033
|
-
const now = Date.now();
|
|
5034
|
-
const base = {
|
|
5035
|
-
configured: cfg !== null,
|
|
5036
|
-
enabled: cfg?.enabled ?? false,
|
|
5037
|
-
intervalHours: Math.max(MIN_INTERVAL_HOURS, cfg?.intervalHours ?? 12),
|
|
5038
|
-
biasProvider: cfg?.biasProvider?.trim() || "autopilot-bias.ts",
|
|
5039
|
-
topPrompt: cfg?.topPrompt?.trim() || null,
|
|
5040
|
-
session: cfg?.session ?? null,
|
|
5041
|
-
avoidWakeHours: {
|
|
5042
|
-
start: cfg?.avoidWakeHours?.start ?? DEFAULT_AVOID_HOURS.start,
|
|
5043
|
-
end: cfg?.avoidWakeHours?.end ?? DEFAULT_AVOID_HOURS.end
|
|
5044
|
-
},
|
|
5045
|
-
beijingHour: beijingHour(now),
|
|
5046
|
-
windowAllowed: inAllowedWakeWindow(now, cfg?.avoidWakeHours),
|
|
5047
|
-
recentWakes: wakeHistoryFor(root).slice(-10).reverse()
|
|
5048
4095
|
};
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
4096
|
+
const promotedBy = (entry) => entry.signalCount >= requiredSignals || entry.rounds >= maxRoundsFallback;
|
|
4097
|
+
const scan = (session) => {
|
|
4098
|
+
let boundary = -1;
|
|
4099
|
+
let signalCount = 0;
|
|
4100
|
+
let rounds = 0;
|
|
4101
|
+
const events = sessionEvents(session);
|
|
4102
|
+
if (events.length > 0) for (const event of events) {
|
|
4103
|
+
const e = event;
|
|
4104
|
+
const seq = typeof e.seq === "number" ? e.seq : 0;
|
|
4105
|
+
if (e.type === "compaction/end") {
|
|
4106
|
+
boundary = seq;
|
|
4107
|
+
signalCount = 0;
|
|
4108
|
+
rounds = 0;
|
|
4109
|
+
continue;
|
|
4110
|
+
}
|
|
4111
|
+
if (promoteEvents.has(e.type) && seq > boundary) signalCount++;
|
|
4112
|
+
if (e.type === "step/start" && seq > boundary) rounds++;
|
|
4113
|
+
}
|
|
4114
|
+
const entry = {
|
|
4115
|
+
boundary,
|
|
4116
|
+
signalCount,
|
|
4117
|
+
rounds
|
|
4118
|
+
};
|
|
4119
|
+
const sid = sessionIdOf(session);
|
|
4120
|
+
if (sid) state.set(sid, entry);
|
|
4121
|
+
return {
|
|
4122
|
+
boundary,
|
|
4123
|
+
promoted: promotedBy(entry)
|
|
4124
|
+
};
|
|
5057
4125
|
};
|
|
5058
|
-
let idleHours = 0;
|
|
5059
|
-
try {
|
|
5060
|
-
idleHours = (now - statSync(mdPath).mtimeMs) / 36e5;
|
|
5061
|
-
} catch {}
|
|
5062
4126
|
return {
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
4127
|
+
status(agent) {
|
|
4128
|
+
if (agent === void 0) return {
|
|
4129
|
+
boundary: -1,
|
|
4130
|
+
promoted: true
|
|
4131
|
+
};
|
|
4132
|
+
const session = agent.session;
|
|
4133
|
+
if (session === void 0) return {
|
|
4134
|
+
boundary: -1,
|
|
4135
|
+
promoted: true
|
|
4136
|
+
};
|
|
4137
|
+
const loopSid = session.id;
|
|
4138
|
+
if (typeof loopSid === "string" && (loopSid.startsWith("handyman-") || isSkiffSessionId(loopSid))) return {
|
|
4139
|
+
boundary: -1,
|
|
4140
|
+
promoted: true
|
|
4141
|
+
};
|
|
4142
|
+
if ((session.header?.delegationDepth ?? 0) > 0) return {
|
|
4143
|
+
boundary: -1,
|
|
4144
|
+
promoted: true
|
|
4145
|
+
};
|
|
4146
|
+
const sid = sessionIdOf(session);
|
|
4147
|
+
if (sid === void 0) return {
|
|
4148
|
+
boundary: -1,
|
|
4149
|
+
promoted: true
|
|
4150
|
+
};
|
|
4151
|
+
const s = state.get(sid);
|
|
4152
|
+
if (s) return {
|
|
4153
|
+
boundary: s.boundary,
|
|
4154
|
+
promoted: promotedBy(s)
|
|
4155
|
+
};
|
|
4156
|
+
return scan(session);
|
|
4157
|
+
},
|
|
4158
|
+
observe(session, event) {
|
|
4159
|
+
const sid = sessionIdOf(session);
|
|
4160
|
+
if (sid === void 0) return;
|
|
4161
|
+
const entry = state.get(sid);
|
|
4162
|
+
if (entry === void 0) return;
|
|
4163
|
+
const e = event;
|
|
4164
|
+
const seq = typeof e.seq === "number" ? e.seq : 0;
|
|
4165
|
+
if (e.type === "compaction/end") {
|
|
4166
|
+
state.set(sid, {
|
|
4167
|
+
boundary: seq,
|
|
4168
|
+
signalCount: 0,
|
|
4169
|
+
rounds: 0
|
|
4170
|
+
});
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
if (promoteEvents.has(e.type) && seq > entry.boundary) entry.signalCount++;
|
|
4174
|
+
if (e.type === "step/start" && seq > entry.boundary) entry.rounds++;
|
|
4175
|
+
state.set(sid, entry);
|
|
5069
4176
|
}
|
|
5070
4177
|
};
|
|
5071
4178
|
}
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
cwd,
|
|
5082
|
-
cccRoot: cwd ? findSerenityRoot(cwd) : null,
|
|
5083
|
-
title: readSessionTitle(s)
|
|
5084
|
-
});
|
|
5085
|
-
}
|
|
5086
|
-
} catch {}
|
|
5087
|
-
return out;
|
|
5088
|
-
}
|
|
5089
|
-
/**
|
|
5090
|
-
* 收集所有「配置了 Autopilot Trajectory 的 live CCC」——**v1.27.4 多 CCC 独立**:
|
|
5091
|
-
* 返回 enabled 的 CCC 列表(时钟 tick 遍历目标);无 enabled → []。
|
|
5092
|
-
* (原 resolveAutoTrajectoryCcc 单目标语义被 collect 取代——面板/诊断用 enableOnly=false 取全量)
|
|
5093
|
-
*/
|
|
5094
|
-
function collectAutopilotCccs(ctx, opts = {}) {
|
|
5095
|
-
const enabled = [];
|
|
5096
|
-
const configured = [];
|
|
5097
|
-
for (const s of listLiveSessions(ctx)) {
|
|
5098
|
-
if (!s.cccRoot) continue;
|
|
5099
|
-
const cfg = readAutopilotSettings(s.cccRoot);
|
|
5100
|
-
if (!cfg) continue;
|
|
5101
|
-
if (cfg.enabled) {
|
|
5102
|
-
if (!enabled.includes(s.cccRoot)) enabled.push(s.cccRoot);
|
|
5103
|
-
} else if (!configured.includes(s.cccRoot)) configured.push(s.cccRoot);
|
|
5104
|
-
}
|
|
5105
|
-
return opts.includeDisabled ? [...enabled, ...configured] : enabled;
|
|
5106
|
-
}
|
|
5107
|
-
function diagLive(ctx) {
|
|
5108
|
-
const liveSessions = listLiveSessions(ctx);
|
|
5109
|
-
const processCwd = process.cwd();
|
|
5110
|
-
const processCcc = findSerenityRoot(processCwd);
|
|
5111
|
-
const panelResolved = collectAutopilotCccs(ctx)[0] ?? resolveAutopilotRoot(ctx);
|
|
5112
|
-
const autopilotCccs = [];
|
|
5113
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5114
|
-
for (const s of liveSessions) {
|
|
5115
|
-
if (!s.cccRoot || seen.has(s.cccRoot)) continue;
|
|
5116
|
-
const cfg = readAutopilotSettings(s.cccRoot);
|
|
5117
|
-
if (!cfg) continue;
|
|
5118
|
-
seen.add(s.cccRoot);
|
|
5119
|
-
const status = getAutopilotStatus(s.cccRoot);
|
|
5120
|
-
const mdPath = resolveTargetMd(s.cccRoot, cfg);
|
|
5121
|
-
const agentResolved = mdPath ? resolveTargetAgent(ctx, mdPath) !== null : false;
|
|
5122
|
-
const agentDiagnosis = mdPath && !agentResolved ? diagnoseTargetUnavailable(ctx, mdPath) : null;
|
|
5123
|
-
autopilotCccs.push({
|
|
5124
|
-
root: s.cccRoot,
|
|
5125
|
-
enabled: status.enabled,
|
|
5126
|
-
session: status.session,
|
|
5127
|
-
target: status.target,
|
|
5128
|
-
agentResolved,
|
|
5129
|
-
agentDiagnosis
|
|
5130
|
-
});
|
|
5131
|
-
}
|
|
4179
|
+
const PROMOTE_EVENTS = {
|
|
4180
|
+
"tool-call": /* @__PURE__ */ new Set(["tool/call"]),
|
|
4181
|
+
"assistant-message": /* @__PURE__ */ new Set(["assistant/message"]),
|
|
4182
|
+
either: /* @__PURE__ */ new Set(["tool/call", "assistant/message"])
|
|
4183
|
+
};
|
|
4184
|
+
/** 协议固有 bootstrap 设置:zeroTools=true(首请求 0 工具纯文字锚定轮)、
|
|
4185
|
+
* 晋升信号仅 assistant/message(对齐 zero-anchored-standard)、
|
|
4186
|
+
* requiredSignals = 锚定轮数(2 条消息 → 2 条回复后晋升)。无参——零配置面。 */
|
|
4187
|
+
function resolveBootstrapSettings() {
|
|
5132
4188
|
return {
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
4189
|
+
bootstrapTools: [...DEFAULT_BOOTSTRAP_TOOLS],
|
|
4190
|
+
promoteEvents: PROMOTE_EVENTS["assistant-message"],
|
|
4191
|
+
requiredSignals: DEFAULT_ANCHOR_MESSAGES.length,
|
|
4192
|
+
suppressedSources: new Set(DEFAULT_SUPPRESSED_SOURCES),
|
|
4193
|
+
compactionTools: [...DEFAULT_COMPACTION_TOOLS],
|
|
4194
|
+
anchorMessages: [...DEFAULT_ANCHOR_MESSAGES],
|
|
4195
|
+
zeroTools: true
|
|
5138
4196
|
};
|
|
5139
4197
|
}
|
|
5140
|
-
//#endregion
|
|
5141
|
-
//#region src/tools/autopilot-trajectory.ts
|
|
5142
4198
|
/**
|
|
5143
|
-
*
|
|
5144
|
-
*
|
|
5145
|
-
*
|
|
5146
|
-
*
|
|
5147
|
-
* agent
|
|
5148
|
-
* 实际执行(写配置/写偏见脚本/标记会话)由 CCC 自己决定、自己用现有工具完成。
|
|
4199
|
+
* 注册 Anchored bootstrap 机制(协议固有,零配置面——S142;按 agent cwd 定位 CCC root,
|
|
4200
|
+
* 晋升状态按 root 隔离,但设置全局一致):
|
|
4201
|
+
* 1. session/event 观察晋升信号(assistant/message + step/start 兜底 + compaction/end)
|
|
4202
|
+
* 2. system-prompt/assemble:bootstrap 阶段目录窄化到 bootstrapTools(+compaction 后 compactionTools)
|
|
4203
|
+
* 3. agent/pre-step:bootstrap 阶段剥离 suppressedSources 注入消息(降级保留全部)
|
|
5149
4204
|
*
|
|
5150
|
-
*
|
|
5151
|
-
* 环境注入 SERENITY_ROOT(当前 CCC 根)供脚本定位;bun 优先(可直跑 TS)。
|
|
5152
|
-
* diag-live:**进程内诊断**(v1.26.14 用户"排查访问不到")——闭包捕获 ctx,
|
|
5153
|
-
* 直接读 live 会话/标题/agent(脚本 diag 看不到运行时),输出实例级诊断报告。
|
|
5154
|
-
*/
|
|
5155
|
-
/**
|
|
5156
|
-
* 定位包内脚本(npm files 分发 experiments/autopilot-trajectory/)。
|
|
5157
|
-
* 布局差异:tsdown bundle 后 import.meta.url 指向 lib/index.js(lib → 包根 1 层);
|
|
5158
|
-
* vitest 源码直跑时指向 src/tools/x.ts(src/tools → 包根 2 层)——逐级上溯查找,
|
|
5159
|
-
* 两种布局都稳(找到 experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts 即止)。
|
|
4205
|
+
* 摘除 = 删除 index.ts 中本注册行(独立模块)。
|
|
5160
4206
|
*/
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
const parent = dirname(cur);
|
|
5167
|
-
if (parent === cur) return null;
|
|
5168
|
-
cur = parent;
|
|
5169
|
-
}
|
|
5170
|
-
}
|
|
5171
|
-
/** 包内脚本(上溯查找;找不到 → execute 报错提示包完整性) */
|
|
5172
|
-
const EXP_SCRIPT = findExpScript(dirname(fileURLToPath(import.meta.url)));
|
|
5173
|
-
const AUTOPILOT_ACTIONS = [
|
|
5174
|
-
"all",
|
|
5175
|
-
"init",
|
|
5176
|
-
"random",
|
|
5177
|
-
"diag",
|
|
5178
|
-
"doc",
|
|
5179
|
-
"check",
|
|
5180
|
-
"status",
|
|
5181
|
-
"guide",
|
|
5182
|
-
"diag-live"
|
|
5183
|
-
];
|
|
5184
|
-
function agentCwd$1(exec) {
|
|
5185
|
-
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
5186
|
-
}
|
|
5187
|
-
function renderText(value) {
|
|
5188
|
-
return [{
|
|
5189
|
-
type: "text",
|
|
5190
|
-
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
5191
|
-
}];
|
|
5192
|
-
}
|
|
5193
|
-
/** 渲染 diag-live 报告为可读文本(EAP:结构清晰、命中点显式) */
|
|
5194
|
-
function renderDiagLive(r) {
|
|
5195
|
-
const lines = [
|
|
5196
|
-
"═══ Autopilot Trajectory 进程内诊断(diag-live,v1.26.14)═══",
|
|
5197
|
-
`进程 cwd: ${r.processCwd}(CCC: ${r.processCcc ?? "无 .serenity"})`,
|
|
5198
|
-
`面板解析(无参 GET 目标): ${r.panelResolved ?? "无配置 Autopilot 的 live CCC"}`,
|
|
5199
|
-
"",
|
|
5200
|
-
`── live 会话(${r.liveSessions.length})──`
|
|
5201
|
-
];
|
|
5202
|
-
if (r.liveSessions.length === 0) lines.push(" (无 live 会话——WebUI 未打开任何会话;定时器绑定依赖 live 会话 cwd)");
|
|
5203
|
-
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}` : ""}`);
|
|
5204
|
-
lines.push("", `── 配置了 Autopilot Trajectory 的 CCC(${r.autopilotCccs.length})──`);
|
|
5205
|
-
if (r.autopilotCccs.length === 0) lines.push(" (无——live 会话中无任何 CCC 配置 autopilotTrajectory)");
|
|
5206
|
-
for (const c of r.autopilotCccs) {
|
|
5207
|
-
lines.push(` · ${c.root}(enabled=${c.enabled})`);
|
|
5208
|
-
lines.push(` session=${c.session ?? "未配置"} | 目标=${c.target ? `${c.target.dirName}(--auto ${c.target.autoFlag ? "✓" : "✗"},空闲 ${c.target.idleHours.toFixed(1)}h)` : "未命中"}`);
|
|
5209
|
-
lines.push(` agent 定位: ${c.agentResolved ? "✓ 可注入" : `✗ 不可得——${c.agentDiagnosis ?? "未知"}`}`);
|
|
5210
|
-
}
|
|
5211
|
-
return lines.join("\n");
|
|
4207
|
+
/** 从 agent 解析 CCC 根(宽松;无 agent/无 CCC 返回 null) */
|
|
4208
|
+
function agentRoot(agent) {
|
|
4209
|
+
const cwd = agent?.session?.header?.cwd;
|
|
4210
|
+
if (typeof cwd !== "string") return null;
|
|
4211
|
+
return findSerenityRoot(cwd);
|
|
5212
4212
|
}
|
|
5213
|
-
/**
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
const
|
|
5232
|
-
if (!
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
const
|
|
5242
|
-
if (
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
4213
|
+
/** 协议固有设置(零配置面):所有 CCC 使用同一固化设置 */
|
|
4214
|
+
const SETTINGS = resolveBootstrapSettings();
|
|
4215
|
+
function registerBootstrap(ctx) {
|
|
4216
|
+
const trackers = /* @__PURE__ */ new Map();
|
|
4217
|
+
/** 已锚定会话(进程内防重:子 agent 每会话只锚定一次) */
|
|
4218
|
+
const anchoredSessions = /* @__PURE__ */ new Set();
|
|
4219
|
+
const trackerFor = (root) => {
|
|
4220
|
+
let t = trackers.get(root);
|
|
4221
|
+
if (!t) {
|
|
4222
|
+
t = createEpochPromotion(SETTINGS.promoteEvents, SETTINGS.requiredSignals);
|
|
4223
|
+
trackers.set(root, t);
|
|
4224
|
+
}
|
|
4225
|
+
return t;
|
|
4226
|
+
};
|
|
4227
|
+
ctx.on("session/event", (session, event) => {
|
|
4228
|
+
try {
|
|
4229
|
+
const cwd = session?.header?.cwd;
|
|
4230
|
+
if (typeof cwd !== "string") return;
|
|
4231
|
+
const root = findSerenityRoot(cwd);
|
|
4232
|
+
if (!root) return;
|
|
4233
|
+
trackerFor(root).observe(session, event);
|
|
4234
|
+
} catch {}
|
|
4235
|
+
});
|
|
4236
|
+
ctx.on("agent/inbox/inserted", ({ agent, message }) => {
|
|
4237
|
+
try {
|
|
4238
|
+
if (!agentRoot(agent)) return;
|
|
4239
|
+
const session = agent.session;
|
|
4240
|
+
const depth = session?.header?.delegationDepth ?? 0;
|
|
4241
|
+
const sid = typeof session?.id === "string" ? session.id : void 0;
|
|
4242
|
+
if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
|
|
4243
|
+
if (depth === 0) {
|
|
4244
|
+
if (hasUserMessageHistory(session)) return;
|
|
4245
|
+
} else if (sid !== void 0 && anchoredSessions.has(sid)) return;
|
|
4246
|
+
if (message?.source?.kind === "plugin") return;
|
|
4247
|
+
const inbox = agent.inbox;
|
|
4248
|
+
if (!inbox?.prepend) return;
|
|
4249
|
+
for (let i = SETTINGS.anchorMessages.length - 1; i >= 0; i--) inbox.prepend("next-turn", {
|
|
4250
|
+
id: `bootstrap-anchor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}-${i}`,
|
|
4251
|
+
role: "user",
|
|
4252
|
+
content: [{
|
|
4253
|
+
type: "text",
|
|
4254
|
+
text: SETTINGS.anchorMessages[i]
|
|
4255
|
+
}],
|
|
4256
|
+
source: {
|
|
4257
|
+
kind: "plugin",
|
|
4258
|
+
plugin: "dsh-serenity-hooks",
|
|
4259
|
+
form: "notice",
|
|
4260
|
+
summary: "bootstrap anchor turn"
|
|
4261
|
+
}
|
|
5252
4262
|
});
|
|
5253
|
-
if (
|
|
5254
|
-
|
|
5255
|
-
|
|
4263
|
+
if (sid !== void 0) anchoredSessions.add(sid);
|
|
4264
|
+
warnOnce(`anchor turns injected: ${SETTINGS.anchorMessages.length} 条(${SETTINGS.anchorMessages[0].slice(0, 40)}…)`);
|
|
4265
|
+
} catch {}
|
|
4266
|
+
});
|
|
4267
|
+
let warned = false;
|
|
4268
|
+
const warnOnce = (message) => {
|
|
4269
|
+
if (warned) return;
|
|
4270
|
+
warned = true;
|
|
4271
|
+
try {
|
|
4272
|
+
console.warn(`[serenity-hooks] bootstrap: ${message}`);
|
|
4273
|
+
} catch {}
|
|
4274
|
+
};
|
|
4275
|
+
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
4276
|
+
const assembled = await next();
|
|
4277
|
+
try {
|
|
4278
|
+
const agent = context.agent;
|
|
4279
|
+
const root = agentRoot(agent);
|
|
4280
|
+
if (!root) return assembled;
|
|
4281
|
+
const status = trackerFor(root).status(agent);
|
|
4282
|
+
if (status.promoted) return assembled;
|
|
4283
|
+
const tools = assembled.tools;
|
|
4284
|
+
if (!Array.isArray(tools)) return assembled;
|
|
4285
|
+
const available = new Set(tools.map((tool) => tool.name).filter((n) => typeof n === "string"));
|
|
4286
|
+
if (SETTINGS.zeroTools) {
|
|
4287
|
+
if (status.boundary < 0) return {
|
|
4288
|
+
...assembled,
|
|
4289
|
+
tools: []
|
|
4290
|
+
};
|
|
4291
|
+
const keep = new Set(SETTINGS.compactionTools);
|
|
4292
|
+
const missing = [...keep].filter((name) => !available.has(name));
|
|
4293
|
+
if (missing.length > 0) {
|
|
4294
|
+
warnOnce(`expected compaction tools missing=${JSON.stringify(missing)} — bootstrap disabled, full catalog exposed`);
|
|
4295
|
+
return assembled;
|
|
4296
|
+
}
|
|
4297
|
+
return {
|
|
4298
|
+
...assembled,
|
|
4299
|
+
tools: tools.filter((tool) => typeof tool.name === "string" && keep.has(tool.name))
|
|
4300
|
+
};
|
|
5256
4301
|
}
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
4302
|
+
const keep = new Set(SETTINGS.bootstrapTools);
|
|
4303
|
+
if (status.boundary >= 0) for (const toolName of SETTINGS.compactionTools) keep.add(toolName);
|
|
4304
|
+
const missing = [...keep].filter((name) => !available.has(name));
|
|
4305
|
+
if (missing.length > 0) {
|
|
4306
|
+
warnOnce(`expected bootstrap tools missing=${JSON.stringify(missing)} — bootstrap disabled, full catalog exposed`);
|
|
4307
|
+
return assembled;
|
|
5260
4308
|
}
|
|
5261
|
-
|
|
5262
|
-
|
|
4309
|
+
return {
|
|
4310
|
+
...assembled,
|
|
4311
|
+
tools: tools.filter((tool) => typeof tool.name === "string" && keep.has(tool.name))
|
|
4312
|
+
};
|
|
4313
|
+
} catch (error) {
|
|
4314
|
+
warnOnce(`assemble filter failed, exposing the full catalog: ${String(error?.message ?? error)}`);
|
|
4315
|
+
return assembled;
|
|
5263
4316
|
}
|
|
5264
4317
|
});
|
|
4318
|
+
ctx.on("agent/pre-step", async ({ agent }, next) => {
|
|
4319
|
+
const decision = await next();
|
|
4320
|
+
if (decision.kind === "reject") return decision;
|
|
4321
|
+
try {
|
|
4322
|
+
const root = agentRoot(agent);
|
|
4323
|
+
if (!root) return decision;
|
|
4324
|
+
if (trackerFor(root).status(agent).promoted || SETTINGS.suppressedSources.size === 0) return decision;
|
|
4325
|
+
const messages = decision.messages;
|
|
4326
|
+
if (!Array.isArray(messages)) return decision;
|
|
4327
|
+
const kept = messages.filter((message) => {
|
|
4328
|
+
const kind = message?.source?.kind;
|
|
4329
|
+
return typeof kind !== "string" || !SETTINGS.suppressedSources.has(kind);
|
|
4330
|
+
});
|
|
4331
|
+
return kept.length === messages.length ? decision : {
|
|
4332
|
+
...decision,
|
|
4333
|
+
messages: kept
|
|
4334
|
+
};
|
|
4335
|
+
} catch (error) {
|
|
4336
|
+
warnOnce(`pre-step context filter failed, keeping injected context: ${String(error?.message ?? error)}`);
|
|
4337
|
+
return decision;
|
|
4338
|
+
}
|
|
4339
|
+
}, { prepend: true });
|
|
4340
|
+
}
|
|
4341
|
+
//#endregion
|
|
4342
|
+
//#region src/trajectory-assistant.ts
|
|
4343
|
+
/**
|
|
4344
|
+
* trajectory-assistant.ts — 关卡化注入的统一命名与风格门面(v0.3,S142)
|
|
4345
|
+
*
|
|
4346
|
+
* 概念(用户拍板 2026-09-05):dsp 全部"过程中动态提示注入"统一命名为
|
|
4347
|
+
* trajectory-assistant(轨迹助航员)。关卡设计思想塑造**结构与时机**
|
|
4348
|
+
* (何时/何地注入),**不用于提示词用词**(D8 词法原则)——纯游戏黑话
|
|
4349
|
+
* (BOSS/XP/level-up)禁止出现在提示词文本;CHECKPOINT/LIMIT/TUTORIAL 等
|
|
4350
|
+
* 跨领域自然通用词允许。
|
|
4351
|
+
*
|
|
4352
|
+
* 本模块 = token 常量 + level-event 词汇表的**单一真相源**(避免前缀散落
|
|
4353
|
+
* 各文件字符串字面量)+ 风格门面(plain 默认 / metaphor 星舰变体)。
|
|
4354
|
+
* 仅当 style=metaphor 时前缀包装措辞变化;可行动正文不变 → 零行为漂移。
|
|
4355
|
+
*
|
|
4356
|
+
* 设计简写(L0 环境 / L1 教程 / L2 目标 / L3 检查点 / L4 极限 / L5 守卫 /
|
|
4357
|
+
* L6 结算)仅在本模块注释与内部文档出现,永不浮现在提示词文本。
|
|
4358
|
+
*
|
|
4359
|
+
* 结算(settlement)挂点:D6 用户拍板——若 CCC 有标准 SESSION 复盘仪式,
|
|
4360
|
+
* 结算视图归 trajectory-assistant。当前 close/archive 无复盘摘要 → 机制
|
|
4361
|
+
* 延后,仅留 onSettlement 导出 seam(OP-1,无调用者)。
|
|
4362
|
+
*/
|
|
4363
|
+
/** 可见提示词前缀用词(跨领域自然词,D8:无游戏黑话) */
|
|
4364
|
+
const EVENT_LABEL = {
|
|
4365
|
+
/** 计分同步提醒(原 TRAJECTORY-STEWARD) */
|
|
4366
|
+
checkpoint: "CHECKPOINT",
|
|
4367
|
+
/** 上下文极限重建提醒(原 TRAJECTORY;LIMIT 替代被否的 BOSS——自然词) */
|
|
4368
|
+
limit: "LIMIT",
|
|
4369
|
+
/** 极限强制升级(原 TRAJECTORY-ESCALATED) */
|
|
4370
|
+
limitMandatory: "LIMIT · MANDATORY",
|
|
4371
|
+
/** 重建锚点头部(原 TRAJECTORY-REBUILD) */
|
|
4372
|
+
rebuild: "REBUILD",
|
|
4373
|
+
/** 敏感输出边界守卫(原 SERENITY OUTPUT GUARD) */
|
|
4374
|
+
guard: "BOUNDARY GUARD"
|
|
4375
|
+
};
|
|
4376
|
+
/** 家族标识(所有动态注入统一前缀) */
|
|
4377
|
+
const ASSISTANT_PREFIX = "TRAJECTORY-ASSISTANT";
|
|
4378
|
+
/** 完整 token:`[TRAJECTORY-ASSISTANT · <LABEL>]` */
|
|
4379
|
+
function eventToken(event) {
|
|
4380
|
+
return `[${ASSISTANT_PREFIX} · ${EVENT_LABEL[event]}]`;
|
|
5265
4381
|
}
|
|
4382
|
+
/** ACK 确认码前缀(recorded/skipped 语义不变,仅家族名更新) */
|
|
4383
|
+
const ACK_PREFIX = `${ASSISTANT_PREFIX}-recorded`;
|
|
5266
4384
|
//#endregion
|
|
5267
4385
|
//#region src/seams/keeper.ts
|
|
5268
4386
|
const SCORES = {
|
|
@@ -5274,8 +4392,8 @@ const SCORES = {
|
|
|
5274
4392
|
grep: 1,
|
|
5275
4393
|
glob: 1,
|
|
5276
4394
|
skill: 1,
|
|
5277
|
-
|
|
5278
|
-
|
|
4395
|
+
msm: 1,
|
|
4396
|
+
container_fs: 1
|
|
5279
4397
|
};
|
|
5280
4398
|
function scoreTool(toolName) {
|
|
5281
4399
|
return SCORES[toolName] ?? 0;
|
|
@@ -5323,14 +4441,14 @@ function reminderText(code, score) {
|
|
|
5323
4441
|
* 简短留自由度(不提"加载 eap 工具",模型自知;只说修订 skill + 新建 skill 落 SESSION)。
|
|
5324
4442
|
*
|
|
5325
4443
|
* escalated=true(v1.23.3):连续多轮超阈值仍未 rebuild → 升级强制语气
|
|
5326
|
-
* (STOP and rebuild now,持续注入直到调用
|
|
4444
|
+
* (STOP and rebuild now,持续注入直到调用 logbook rebuild)。
|
|
5327
4445
|
*
|
|
5328
4446
|
* 需求①(S142 用户拍板):百分比比例 → K 数值——tokensK = 实际占用(千 token),
|
|
5329
4447
|
* thresholdK = 配置阈值(千 token);文案 `Context usage at NNNK (threshold NNNK)`。
|
|
5330
4448
|
*/
|
|
5331
4449
|
function rebuildReminderText(tokensK, thresholdK, escalated = false) {
|
|
5332
|
-
if (escalated) return `${eventToken("limitMandatory")} Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K) — you have been reminded repeatedly and have NOT called
|
|
5333
|
-
return `${eventToken("limit")} Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K). This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy. Before rebuilding: if this conversation produced valuable cognition, revise the relevant existing skill of this CCC (structure it with eap); if a new skill is warranted, write a short proposal into SESSION.md for the user to review — do not create it yourself. ACT NOW: at the next natural pause (end of the current task step), call the
|
|
4450
|
+
if (escalated) return `${eventToken("limitMandatory")} Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K) — you have been reminded repeatedly and have NOT called the logbook rebuild action. This is now mandatory: STOP at the current task step, preserve valuable cognition into the CCC skills (or write a new-skill proposal into SESSION.md), then call logbook rebuild immediately, passing --summary "<content summary ≤20 chars>" (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild). The conversation will be cleared and rebuilt in place; SESSION.md is the persistent trajectory and stays in place — identity continues from it. Do not continue working without rebuilding; this reminder persists until you call logbook rebuild.`;
|
|
4451
|
+
return `${eventToken("limit")} Context usage at ${Math.round(tokensK)}K (threshold ${Math.round(thresholdK)}K). This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy. Before rebuilding: if this conversation produced valuable cognition, revise the relevant existing skill of this CCC (structure it with eap); if a new skill is warranted, write a short proposal into SESSION.md for the user to review — do not create it yourself. ACT NOW: at the next natural pause (end of the current task step), call the logbook rebuild action — passing --summary "<content summary ≤20 chars>" describing the next work phase (required; the dsh session title is renamed to S###-YYYY-MM-DD-<summary> after rebuild) — to clear and rebuild this conversation: the current copy is discarded, identity continues from SESSION.md. If you are in the middle of an unbreakable step, continue it, then rebuild at its end. Do not ignore this; rebuild is the expected action, not an option.`;
|
|
5334
4452
|
}
|
|
5335
4453
|
/** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
|
|
5336
4454
|
function readContextPressure(ctx, session) {
|
|
@@ -5347,7 +4465,7 @@ function readContextPressure(ctx, session) {
|
|
|
5347
4465
|
return null;
|
|
5348
4466
|
}
|
|
5349
4467
|
}
|
|
5350
|
-
const PLUGIN_SOURCE$
|
|
4468
|
+
const PLUGIN_SOURCE$3 = {
|
|
5351
4469
|
kind: "plugin",
|
|
5352
4470
|
plugin: "dsh-serenity-hooks"
|
|
5353
4471
|
};
|
|
@@ -5411,7 +4529,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
5411
4529
|
if (blocks.length === 0) return downstream;
|
|
5412
4530
|
const reminderAll = createUserMessage({
|
|
5413
4531
|
content: blocks,
|
|
5414
|
-
source: PLUGIN_SOURCE$
|
|
4532
|
+
source: PLUGIN_SOURCE$3
|
|
5415
4533
|
});
|
|
5416
4534
|
if (downstream.kind === "block") return {
|
|
5417
4535
|
kind: "block",
|
|
@@ -5930,7 +5048,7 @@ function identityBlock(root) {
|
|
|
5930
5048
|
"",
|
|
5931
5049
|
"The DSH platform tools remain available too (read/write/edit/glob/grep/web_search/ask_user_question/subagent/workflow/goal and more) — the ACC tools are the serenity-native layer, not the only tools.",
|
|
5932
5050
|
"",
|
|
5933
|
-
"Additional MSMs registered by this CCC are available — call
|
|
5051
|
+
"Additional MSMs registered by this CCC are available — call msm(\"<name>\") to execute or discover them (see the \"Serenity Tools\" heading below).",
|
|
5934
5052
|
""
|
|
5935
5053
|
].join("\n");
|
|
5936
5054
|
}
|
|
@@ -5939,7 +5057,7 @@ function identityBlock(root) {
|
|
|
5939
5057
|
* 原 accBlock 内嵌 13 行工具清单 → 独立成块放装配末尾(SKILL 后、Session 前)。
|
|
5940
5058
|
* 身份先行(认知轨迹开头不被 13 行清单干扰)、工具参考殿后(需要时再看)。
|
|
5941
5059
|
* 附带 MSM 调用示例(用户拍板:顶层提示词加调用方式说明,避免偶发调用错误——
|
|
5942
|
-
* 模型对
|
|
5060
|
+
* 模型对 msm 参数面/协议 flag 理解不稳)。
|
|
5943
5061
|
*/
|
|
5944
5062
|
function toolsBlock() {
|
|
5945
5063
|
return [
|
|
@@ -5947,27 +5065,22 @@ function toolsBlock() {
|
|
|
5947
5065
|
"=== Serenity Tools ===",
|
|
5948
5066
|
"The ACC (this plugin) provides the following built-in tools:",
|
|
5949
5067
|
"",
|
|
5950
|
-
"
|
|
5951
|
-
"
|
|
5952
|
-
"
|
|
5953
|
-
"
|
|
5954
|
-
"
|
|
5955
|
-
"
|
|
5956
|
-
"
|
|
5957
|
-
"
|
|
5958
|
-
"
|
|
5959
|
-
" session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
|
|
5960
|
-
" localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
|
|
5961
|
-
" skiff_admin — Skiff (F4, experimental): CCC cognitive-subset roles — guide (definition tutorial) / validate (config check) / apply (validate + confirm live) / list (role summary)",
|
|
5068
|
+
" container_fs — container filesystem operations (15 subcommands: root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
|
|
5069
|
+
" logbook — the voyage's logbook: work-session lifecycle (list/show/create/use/close/health/qa/archive/summary/rebuild/hook-develop-guide)",
|
|
5070
|
+
" dashboard — always-on container instruments: health (CCC three-principle check + registry integrity) / time (now) / wait (N seconds)",
|
|
5071
|
+
" container_git— git operations (status/commit/push/log)",
|
|
5072
|
+
" msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
|
|
5073
|
+
" praxis — actionable theory injection: praxis (index) / praxis eap / praxis neat / praxis cce",
|
|
5074
|
+
" handyman — delegate a do-everything worker agent (CCC-whitelisted model) to run synchronously in rounds until done; jobs=[] orchestrates parallel work",
|
|
5075
|
+
" localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
|
|
5076
|
+
" container_admin — container administration (the maintenance bay): role (Skiff roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config",
|
|
5962
5077
|
" autopilot-trajectory — Autopilot Trajectory one-stop management (all/init/random/diag/doc/check/status/guide)",
|
|
5963
5078
|
"",
|
|
5964
|
-
"
|
|
5965
|
-
"",
|
|
5966
|
-
"
|
|
5967
|
-
"
|
|
5968
|
-
"
|
|
5969
|
-
" 3. Execute: acc_msm exec <name> <args...> — run the MSM with business args (first arg may be --list/--schema/--format=json)",
|
|
5970
|
-
" Register new MSMs with acc_msm register; deregister with acc_msm deregister (registry is ACC-managed — never edit mech-registry.json directly).",
|
|
5079
|
+
"MSM call (registered CCC MSMs, deterministic Mech & Semi-Mech):",
|
|
5080
|
+
" Execute: msm(\"<name>\", [\"<arg1>\", \"<arg2>\"]) — run a registered MSM directly (partial name returns matching candidates)",
|
|
5081
|
+
" Inspect: msm(\"<name>\", [], inspect=true) — view that MSM's usage/flags without running",
|
|
5082
|
+
" Index: msm() — summary of registered MSMs by skill",
|
|
5083
|
+
" Manage: container_admin msm register|deregister|check (registry is ACC-managed — never edit mech-registry.json directly)",
|
|
5971
5084
|
""
|
|
5972
5085
|
].join("\n");
|
|
5973
5086
|
}
|
|
@@ -6042,12 +5155,12 @@ function principlesBlock(root, omitMsmPrinciples = false) {
|
|
|
6042
5155
|
"The session-trajectory relation: a session is the rebuildable carrier of a",
|
|
6043
5156
|
"trajectory. SESSION.md is the trajectory's persistent body — it never moves;",
|
|
6044
5157
|
"the current conversation is a temporary work copy that may be discarded and",
|
|
6045
|
-
"rebuilt (
|
|
5158
|
+
"rebuilt (logbook rebuild). Identity belongs to the trajectory, not to any",
|
|
6046
5159
|
"session.",
|
|
6047
5160
|
""
|
|
6048
5161
|
];
|
|
6049
5162
|
if (!omitMsmPrinciples) lines.push("MSM principles — machinery before improvisation:", "- Determinism first: use a registered Mech before hand-rolling; reserve", " Semi-Mech for genuine judgment points.", "- Single source of truth: an MSM is the only decoder of its own usage", " (--help/--schema); documents must not duplicate it.", "- Registered to act: no tool exists unless it is on the manifest.", "");
|
|
6050
|
-
lines.push("Operational boundaries:", `Root: ${root}`, " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)", " • Shell — use
|
|
5163
|
+
lines.push("Operational boundaries:", `Root: ${root}`, " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)", " • Shell — use msm by default. Note: bash may be disabled", " • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)", " • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm", "");
|
|
6051
5164
|
return lines.join("\n");
|
|
6052
5165
|
}
|
|
6053
5166
|
/**
|
|
@@ -6218,7 +5331,7 @@ function localstoreBlock(root) {
|
|
|
6218
5331
|
"=== Serenity Localstore ===",
|
|
6219
5332
|
"localstore.json is a local private file (gitTrack=deny — not committed to git,",
|
|
6220
5333
|
".gitignore enforced). Credentials/config live only on this machine: do not write",
|
|
6221
|
-
"them into conversation or logs; do not attempt to commit this file (
|
|
5334
|
+
"them into conversation or logs; do not attempt to commit this file (container_git will refuse).",
|
|
6222
5335
|
""
|
|
6223
5336
|
]).join("\n");
|
|
6224
5337
|
}
|
|
@@ -6337,8 +5450,8 @@ function codeModeAdaptationLine(ctx, scope) {
|
|
|
6337
5450
|
return [
|
|
6338
5451
|
"",
|
|
6339
5452
|
"=== Serenity Code Mode ===",
|
|
6340
|
-
"This session renders tools in Code Mode: direct calls to ACC tool names (
|
|
6341
|
-
"Call them inside a single run_code program via the generated SDK bindings: `await tools.
|
|
5453
|
+
"This session renders tools in Code Mode: direct calls to ACC tool names (container_fs/msm etc.) are rejected (UNKNOWN_TOOL).",
|
|
5454
|
+
"Call them inside a single run_code program via the generated SDK bindings: `await tools.container_fs(...)`, `await tools.msm(...)`.",
|
|
6342
5455
|
"The program only returns what you print/return — curate your output.",
|
|
6343
5456
|
""
|
|
6344
5457
|
].join("\n");
|
|
@@ -6451,7 +5564,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
6451
5564
|
}
|
|
6452
5565
|
return lines.join("\n");
|
|
6453
5566
|
}
|
|
6454
|
-
const PLUGIN_SOURCE$
|
|
5567
|
+
const PLUGIN_SOURCE$2 = {
|
|
6455
5568
|
kind: "plugin",
|
|
6456
5569
|
plugin: "dsh-serenity-hooks"
|
|
6457
5570
|
};
|
|
@@ -6468,7 +5581,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
6468
5581
|
}];
|
|
6469
5582
|
return createUserMessage({
|
|
6470
5583
|
content,
|
|
6471
|
-
source: PLUGIN_SOURCE$
|
|
5584
|
+
source: PLUGIN_SOURCE$2
|
|
6472
5585
|
});
|
|
6473
5586
|
}
|
|
6474
5587
|
/** 每 agent 是否已注入过(进程内存态) */
|
|
@@ -6954,7 +6067,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
6954
6067
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
6955
6068
|
});
|
|
6956
6069
|
const root = findSerenityRoot(workspace) ?? "";
|
|
6957
|
-
const { discoverCccs } = await import("./skiff-debug-
|
|
6070
|
+
const { discoverCccs } = await import("./skiff-debug-BA8CDeJl.js").then((n) => n.i);
|
|
6958
6071
|
sendJson$1(res, 200, { cccs: await discoverCccs(ctx, root) });
|
|
6959
6072
|
} catch (err) {
|
|
6960
6073
|
sendJson$1(res, 400, { error: err.message ?? String(err) });
|
|
@@ -7080,11 +6193,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7080
6193
|
sendJson$1(res, 400, { error: chk.error });
|
|
7081
6194
|
return;
|
|
7082
6195
|
}
|
|
7083
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6196
|
+
const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
7084
6197
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
7085
6198
|
const settings = readWeixinSettings(chk.root);
|
|
7086
6199
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
7087
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
6200
|
+
const { readWeixinCredential } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
7088
6201
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
7089
6202
|
accountId: a.accountId,
|
|
7090
6203
|
name: a.name ?? void 0,
|
|
@@ -7116,7 +6229,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7116
6229
|
const { fetchQRCode } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
|
|
7117
6230
|
const { randomUUID } = await import("node:crypto");
|
|
7118
6231
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
7119
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6232
|
+
const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
7120
6233
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
7121
6234
|
const loginKey = randomUUID();
|
|
7122
6235
|
weixinLogins.set(loginKey, {
|
|
@@ -7137,7 +6250,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7137
6250
|
sendJson$1(res, 400, { error: "missing accountId" });
|
|
7138
6251
|
return;
|
|
7139
6252
|
}
|
|
7140
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
6253
|
+
const { removeWeixinAccount } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
7141
6254
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
7142
6255
|
removeWeixinAccount(root, body.accountId);
|
|
7143
6256
|
syncCccBridge(ctx, root);
|
|
@@ -7150,8 +6263,8 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7150
6263
|
sendJson$1(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
7151
6264
|
return;
|
|
7152
6265
|
}
|
|
7153
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
7154
|
-
const { readSkiffRoles } = await import("./skiff-role-
|
|
6266
|
+
const { saveWeixinRoutes } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
6267
|
+
const { readSkiffRoles } = await import("./skiff-role-LW4tjF9L.js").then((n) => n.c);
|
|
7155
6268
|
const roles = readSkiffRoles(root);
|
|
7156
6269
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
7157
6270
|
sendJson$1(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
|
|
@@ -7163,7 +6276,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7163
6276
|
}
|
|
7164
6277
|
if (body.action === "set-enabled") {
|
|
7165
6278
|
const enabled = body.enabled === true;
|
|
7166
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
6279
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
7167
6280
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
7168
6281
|
const settings = readWeixinSettings(root);
|
|
7169
6282
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -7210,7 +6323,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7210
6323
|
return;
|
|
7211
6324
|
}
|
|
7212
6325
|
const { pollQRStatus } = await import("./weixin-api-OUCuw0Ws.js").then((n) => n.l);
|
|
7213
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
6326
|
+
const { readWeixinSettings } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
7214
6327
|
const settings = readWeixinSettings(login.root);
|
|
7215
6328
|
const status = await pollQRStatus({
|
|
7216
6329
|
baseUrl: void 0,
|
|
@@ -7225,7 +6338,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7225
6338
|
});
|
|
7226
6339
|
return;
|
|
7227
6340
|
}
|
|
7228
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
6341
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-jA0qENOf.js").then((n) => n.u);
|
|
7229
6342
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
7230
6343
|
const accountId = nextWeixinAccountId(settings);
|
|
7231
6344
|
upsertWeixinAccount(login.root, {
|
|
@@ -8231,114 +7344,432 @@ function startGateway(config, getAccounts, getDshCookie) {
|
|
|
8231
7344
|
});
|
|
8232
7345
|
server.listen(port, host);
|
|
8233
7346
|
};
|
|
8234
|
-
listenWithRetry();
|
|
7347
|
+
listenWithRetry();
|
|
7348
|
+
return {
|
|
7349
|
+
server,
|
|
7350
|
+
dispose: () => {
|
|
7351
|
+
server.close();
|
|
7352
|
+
}
|
|
7353
|
+
};
|
|
7354
|
+
}
|
|
7355
|
+
/**
|
|
7356
|
+
* 构造 dsh browser cookie 提供者(v1.28.2 0.1.2-rc.1 BrowserAuth 适配):
|
|
7357
|
+
* 经 ctx.connection(HostConnectionHandle)官方通道在内存换取 authority=127.0.0.1:<mainPort>
|
|
7358
|
+
* 的 cookie 并缓存(createDshCookieProvider 惰性换取)。connection 缺失(旧 dsh/非 web 装配)
|
|
7359
|
+
* → provider 返回 undefined → 反代不注入(旧形态无 BrowserAuth,天然兼容)。
|
|
7360
|
+
*
|
|
7361
|
+
* v1.28.2 修复:**connection 每次调用现取**(不绑定 sync 时点)——gateway sync 在 apply
|
|
7362
|
+
* 早期跑,此时 connection 服务可能尚未装配(web-app 的 URL 打印在其后);首次反代请求
|
|
7363
|
+
* 到达时 connection 必已就绪。cachedProvider 缓存换取结果(同一 connection 实例)。
|
|
7364
|
+
*/
|
|
7365
|
+
function buildDshCookieProvider(ctx, mainPort) {
|
|
7366
|
+
const authority = `127.0.0.1:${mainPort}`;
|
|
7367
|
+
let cachedProvider;
|
|
7368
|
+
let diagnosed = false;
|
|
7369
|
+
return () => {
|
|
7370
|
+
try {
|
|
7371
|
+
const connection = ctx.get?.("connection");
|
|
7372
|
+
if (!connection || typeof connection.authenticatedUrl !== "function" || typeof connection.authorizeIndex !== "function") {
|
|
7373
|
+
if (!diagnosed) {
|
|
7374
|
+
diagnosed = true;
|
|
7375
|
+
console.log("[serenity-hooks] dsh browser-auth 适配: connection ✗ 不可取(不注入 dsh cookie——旧 dsh/非 web 装配兼容)");
|
|
7376
|
+
}
|
|
7377
|
+
return;
|
|
7378
|
+
}
|
|
7379
|
+
if (cachedProvider === void 0) cachedProvider = createDshCookieProvider(connection, authority);
|
|
7380
|
+
const cookie = cachedProvider();
|
|
7381
|
+
if (!diagnosed) {
|
|
7382
|
+
diagnosed = true;
|
|
7383
|
+
console.log(`[serenity-hooks] dsh browser-auth 适配: connection ✓ 可取 + cookie ${cookie === void 0 ? "✗ 换取失败(上游 401 时检查)" : "✓ 已内存换取(注入反代)"}`);
|
|
7384
|
+
}
|
|
7385
|
+
return cookie;
|
|
7386
|
+
} catch {
|
|
7387
|
+
return;
|
|
7388
|
+
}
|
|
7389
|
+
};
|
|
7390
|
+
}
|
|
7391
|
+
/** 注册 gateway(index.ts apply 调用)。
|
|
7392
|
+
* 归属原则(v1.22):gateway 是 plugin 全局能力——enabled 开关读 DSH settings
|
|
7393
|
+
* (readSimpleSettings().gatewayEnabled),host/port/accounts 读 plugin 全局文件
|
|
7394
|
+
* (readAdvancedSettings(),~/.dsh/serenity-hooks.json)。**不依赖任何具体 CCC**。
|
|
7395
|
+
* 幂等 sync:仅当 配置/端口/开关 变化时重建监听器。
|
|
7396
|
+
* 事件驱动:config-updated(/serenity/config PUT 后 emit)+ DSH settings 变化(onChange emit)触发重新 sync。
|
|
7397
|
+
*/
|
|
7398
|
+
function registerGateway(ctx) {
|
|
7399
|
+
let current = null;
|
|
7400
|
+
let lastSig = null;
|
|
7401
|
+
const sync = () => {
|
|
7402
|
+
try {
|
|
7403
|
+
const webServer = ctx.get?.("webServer");
|
|
7404
|
+
if (!webServer?.port) return;
|
|
7405
|
+
const enabled = readSimpleSettings().gatewayEnabled;
|
|
7406
|
+
const settings = readAdvancedSettings();
|
|
7407
|
+
const sig = `${enabled}|${settings.gateway.host}|${settings.gateway.port}|${settings.gateway.accounts.length}|${settings.gateway.workspaces.join(",")}|${webServer.port}`;
|
|
7408
|
+
if (sig === lastSig) return;
|
|
7409
|
+
lastSig = sig;
|
|
7410
|
+
if (current) {
|
|
7411
|
+
current.dispose();
|
|
7412
|
+
current = null;
|
|
7413
|
+
}
|
|
7414
|
+
if (!enabled) {
|
|
7415
|
+
console.log("[serenity-hooks] gateway 已停止(gatewayEnabled=false,可在 dsh 设置面板开启)");
|
|
7416
|
+
return;
|
|
7417
|
+
}
|
|
7418
|
+
current = startGateway({
|
|
7419
|
+
host: settings.gateway.host,
|
|
7420
|
+
port: settings.gateway.port,
|
|
7421
|
+
cookieName: "serenity_session",
|
|
7422
|
+
mainPort: webServer.port,
|
|
7423
|
+
loginDelayMs: 0,
|
|
7424
|
+
allowWorkspaces: settings.gateway.workspaces,
|
|
7425
|
+
cookieSecure: settings.gateway.cookieSecure === true,
|
|
7426
|
+
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate !== false,
|
|
7427
|
+
totpEnabled: settings.gateway.totpEnabled === true
|
|
7428
|
+
}, () => readAdvancedSettings().gateway.accounts, buildDshCookieProvider(ctx, webServer.port));
|
|
7429
|
+
const accounts = settings.gateway.accounts.length;
|
|
7430
|
+
const wsNote = settings.gateway.workspaces.length === 0 ? "" : `;工作区白名单 ${settings.gateway.workspaces.length} 条`;
|
|
7431
|
+
console.log(`[serenity-hooks] gateway 已启动: http://${settings.gateway.host}:${settings.gateway.port} → 127.0.0.1:${webServer.port}` + (accounts === 0 ? "(⚠️ 未配置账号,登录页将提示)" : `(${accounts} 个账号)`) + wsNote);
|
|
7432
|
+
} catch (err) {
|
|
7433
|
+
console.warn(`[serenity-hooks] gateway 同步失败: ${String(err?.message ?? err)}`);
|
|
7434
|
+
}
|
|
7435
|
+
};
|
|
7436
|
+
sync();
|
|
7437
|
+
ctx.on("agent/session-start", (payload) => {
|
|
7438
|
+
try {
|
|
7439
|
+
const cwd = payload.agent?.session?.header?.cwd;
|
|
7440
|
+
if (cwd) {
|
|
7441
|
+
const root = findSerenityRoot(cwd);
|
|
7442
|
+
if (root && migrateLegacyLocalstore(root)) console.log(`[serenity-hooks] ✓ 已迁移 serenityAdvanced(CCC localstore → plugin 全局文件)`);
|
|
7443
|
+
}
|
|
7444
|
+
} catch {}
|
|
7445
|
+
sync();
|
|
7446
|
+
});
|
|
7447
|
+
ctx.on("serenity/config-updated", () => {
|
|
7448
|
+
lastSig = null;
|
|
7449
|
+
sync();
|
|
7450
|
+
});
|
|
7451
|
+
ctx.on("serenity/settings-changed", () => {
|
|
7452
|
+
sync();
|
|
7453
|
+
});
|
|
7454
|
+
}
|
|
7455
|
+
//#endregion
|
|
7456
|
+
//#region src/rebuild.ts
|
|
7457
|
+
var rebuild_exports = /* @__PURE__ */ __exportAll({
|
|
7458
|
+
buildRebuildAnchor: () => buildRebuildAnchor,
|
|
7459
|
+
performRebuild: () => performRebuild,
|
|
7460
|
+
queueRebuild: () => queueRebuild,
|
|
7461
|
+
registerRebuildTurnHook: () => registerRebuildTurnHook,
|
|
7462
|
+
resolveSessionMdPath: () => resolveSessionMdPath,
|
|
7463
|
+
sanitizeFocusLine: () => sanitizeFocusLine,
|
|
7464
|
+
stripAckSuffix: () => stripAckSuffix
|
|
7465
|
+
});
|
|
7466
|
+
const PLUGIN_SOURCE$1 = {
|
|
7467
|
+
kind: "plugin",
|
|
7468
|
+
plugin: "dsh-serenity-hooks"
|
|
7469
|
+
};
|
|
7470
|
+
/** acknowledge 尾句(first-anchor 的确认要求——rebuild 重建后不重走确认轮,直接继续) */
|
|
7471
|
+
const ACK_SUFFIX_RE = /Please simply reply "acknowledge" — no action needed\.\s*$/;
|
|
7472
|
+
/**
|
|
7473
|
+
* 剥离 first-anchor 消息的 acknowledge 尾句(rebuild 锚点保留协议正文但不要求确认回复)。
|
|
7474
|
+
*/
|
|
7475
|
+
function stripAckSuffix(text) {
|
|
7476
|
+
return text.replace(ACK_SUFFIX_RE, "").trimEnd();
|
|
7477
|
+
}
|
|
7478
|
+
/**
|
|
7479
|
+
* 构建重建锚点消息(v1.22.4 定稿语义 + v1.22.5 保留 first-anchor 正文):
|
|
7480
|
+
* 「[TRAJECTORY-REBUILD] + first-anchor 协议正文(去 acknowledge 尾句)+ 继续 {SESSION 名} 的工作」。
|
|
7481
|
+
* v1.29.2(R1):可选 task focus 段——简短任务焦点文字传入重建后会话(不含历史,
|
|
7482
|
+
* 历史完整在 SESSION.md;让重建后的自己第一时间理解本轮焦点,降低冷启动认知成本)。
|
|
7483
|
+
* @param root - CCC 根
|
|
7484
|
+
* @param sessionName - 当前 use 的宁静号 SESSION 名(如 S142);无激活则通用指令
|
|
7485
|
+
* @param activeMdPath - 持久轨迹 SESSION.md 绝对路径(保持原位)
|
|
7486
|
+
* @param anchorMessages - first-anchor 协议消息序列(缺省 DEFAULT_ANCHOR_MESSAGES;可注入测试)
|
|
7487
|
+
* @param focus - 可选任务焦点(单行化 + ≤200 字消毒;无则不输出 focus 段——向后兼容)
|
|
7488
|
+
*/
|
|
7489
|
+
function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DEFAULT_ANCHOR_MESSAGES, focus) {
|
|
7490
|
+
const rel = activeMdPath.startsWith(root) ? activeMdPath.slice(root.length + 1) : activeMdPath;
|
|
7491
|
+
const sessionDir = basename(dirname(activeMdPath));
|
|
7492
|
+
const sessionLine = sessionDir !== "AGENT_SESSIONS" ? `- Serenity session: ${sessionName !== "" ? `${sessionName} (${sessionDir})` : sessionDir}` : null;
|
|
7493
|
+
const focusLine = sanitizeFocusLine(focus);
|
|
7494
|
+
return [
|
|
7495
|
+
`${eventToken("rebuild")} The conversation has been cleared and rebuilt (Ship of Theseus: the carrier is replaced, the trajectory continues).`,
|
|
7496
|
+
"",
|
|
7497
|
+
...anchorMessages.flatMap((text) => [stripAckSuffix(text), ""]),
|
|
7498
|
+
sessionName !== "" ? `Continue the work of ${sessionName}.` : "Continue the current work.",
|
|
7499
|
+
...sessionLine ? [sessionLine] : [],
|
|
7500
|
+
`- Persistent trajectory — SESSION.md path: ${rel}`,
|
|
7501
|
+
` (the trajectory's persistent body — stays in place through rebuilds)`,
|
|
7502
|
+
`- Read that SESSION.md first (goal/decisions/progress/unresolved), then continue from the last checkpoint.`,
|
|
7503
|
+
...focusLine ? [`- Task focus: ${focusLine}`] : []
|
|
7504
|
+
].join("\n");
|
|
7505
|
+
}
|
|
7506
|
+
/**
|
|
7507
|
+
* 任务焦点行消毒(纯函数可单测):单行化(换行/回车 → 空格)+ 控制字符清除 +
|
|
7508
|
+
* ≤200 字截断。空/纯空白 → null(不输出 focus 段)。防 LLM 传多行注入伪造锚点结构。
|
|
7509
|
+
*/
|
|
7510
|
+
function sanitizeFocusLine(focus) {
|
|
7511
|
+
if (!focus) return null;
|
|
7512
|
+
const single = focus.replace(/[\r\n\t]+/g, " ").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").trim();
|
|
7513
|
+
if (single === "") return null;
|
|
7514
|
+
return single.length <= FOCUS_MAX_CHARS ? single : single.slice(0, FOCUS_MAX_CHARS);
|
|
7515
|
+
}
|
|
7516
|
+
/** 任务焦点最大长度(字/码点;200 足够一句话任务焦点——非历史) */
|
|
7517
|
+
const FOCUS_MAX_CHARS = 200;
|
|
7518
|
+
/** 从会话 surface 首条 user 消息(重建锚点)解析 SESSION.md 路径(events 异常/缺失时的兜底) */
|
|
7519
|
+
function parseAnchorMdPath(session) {
|
|
7520
|
+
try {
|
|
7521
|
+
const nodes = [...session.surface.nodes];
|
|
7522
|
+
if (nodes.length === 0) return null;
|
|
7523
|
+
const events = sessionEvents(session);
|
|
7524
|
+
if (events.length === 0) return null;
|
|
7525
|
+
const event = events[nodes[0]];
|
|
7526
|
+
if (!event) return null;
|
|
7527
|
+
const message = deriveEventMessage(event);
|
|
7528
|
+
if (!message) return null;
|
|
7529
|
+
const text = message.content?.map((c) => c.type === "text" ? c.text ?? "" : "").join("") ?? "";
|
|
7530
|
+
return extractSessionMdPathFromText(text);
|
|
7531
|
+
} catch {
|
|
7532
|
+
return null;
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
7535
|
+
/** 从 SESSION.md 绝对路径派生会话名(S###;issue 目录回退完整目录名) */
|
|
7536
|
+
function sessionNameFromMdPath(mdPath) {
|
|
7537
|
+
const dirName = basename(dirname(mdPath));
|
|
7538
|
+
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
7539
|
+
return idMatch ? `S${idMatch[1]}` : dirName;
|
|
7540
|
+
}
|
|
7541
|
+
/**
|
|
7542
|
+
* 稳固的 SESSION.md 定位(多层候选 + 存在性校验,**绝不输出虚假路径**):
|
|
7543
|
+
* ① 内存活跃会话(本会话显式 use 过)→ ② events 恢复(use 标记 + 重建锚点规范行,进程重启后)
|
|
7544
|
+
* → ③ surface 首条锚点(events 异常时兜底)→ ④ AGENT_SESSIONS 约定回退(最新未完成活动目录)。
|
|
7545
|
+
* 每候选 resolve 后 existsSync 校验(相对路径按 root 解析);全部失败返回 null → 调用方报错引导。
|
|
7546
|
+
*/
|
|
7547
|
+
function resolveSessionMdPath(root, scope, session) {
|
|
7548
|
+
const candidates = [];
|
|
7549
|
+
candidates.push(getActiveSessionInfo(scope)?.mdPath ?? null);
|
|
7550
|
+
const events = sessionEvents(session);
|
|
7551
|
+
if (events.length > 0) candidates.push(parseSessionContextFromEvents(events)?.mdPath ?? null);
|
|
7552
|
+
candidates.push(parseAnchorMdPath(session));
|
|
7553
|
+
candidates.push(findLatestActiveSessionMd(root));
|
|
7554
|
+
for (const c of candidates) {
|
|
7555
|
+
if (!c) continue;
|
|
7556
|
+
const abs = c.startsWith(root) ? c : resolve(root, c);
|
|
7557
|
+
if (existsSync(abs)) return abs;
|
|
7558
|
+
}
|
|
7559
|
+
return null;
|
|
7560
|
+
}
|
|
7561
|
+
const pendingRebuilds = /* @__PURE__ */ new Map();
|
|
7562
|
+
/** 陈旧队列存活时长(毫秒):超过则丢弃(turn 异常结束/agent 崩溃时防残留误清空) */
|
|
7563
|
+
const PENDING_TTL_MS = 6e5;
|
|
7564
|
+
let diagState = {
|
|
7565
|
+
lastTs: "",
|
|
7566
|
+
lastSessionId: "",
|
|
7567
|
+
lastEvent: "",
|
|
7568
|
+
queueCount: 0,
|
|
7569
|
+
rebuiltCount: 0,
|
|
7570
|
+
droppedCount: 0,
|
|
7571
|
+
failedCount: 0
|
|
7572
|
+
};
|
|
7573
|
+
function writeRebuildDiag(root, entry) {
|
|
7574
|
+
try {
|
|
7575
|
+
const dir = join(root, "AGENT_SESSIONS");
|
|
7576
|
+
mkdirSync(dir, { recursive: true });
|
|
7577
|
+
const file = join(dir, ".rebuild-diag.json");
|
|
7578
|
+
const s = diagState;
|
|
7579
|
+
if (entry.event === "queued") s.queueCount += 1;
|
|
7580
|
+
if (entry.event === "rebuilt") s.rebuiltCount += 1;
|
|
7581
|
+
if (entry.event === "ttl-dropped") s.droppedCount += 1;
|
|
7582
|
+
if (entry.event === "failed") s.failedCount += 1;
|
|
7583
|
+
s.lastTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
7584
|
+
s.lastSessionId = entry.sessionId;
|
|
7585
|
+
s.lastEvent = entry.event;
|
|
7586
|
+
s.detail = entry.detail;
|
|
7587
|
+
writeFileSync(file, JSON.stringify(s, null, 2) + "\n", "utf-8");
|
|
7588
|
+
} catch {}
|
|
7589
|
+
}
|
|
7590
|
+
/**
|
|
7591
|
+
* 排队一次重建(v1.22.4 定稿语义第一步):
|
|
7592
|
+
* ① 门控校验(rebuild.enabled + 会话定位)
|
|
7593
|
+
* ② 构建锚点(激活会话名 + 持久轨迹路径)
|
|
7594
|
+
* ③ 写入 pending 队列——**不立即改 surface**(等 turn-stopping 执行)
|
|
7595
|
+
* @returns 排队结果;rebuild.enabled=false 或非 CCC 抛错
|
|
7596
|
+
*/
|
|
7597
|
+
async function queueRebuild(ctx, opts) {
|
|
7598
|
+
if (!readSimpleSettings().rebuildEnabled) throw new Error("logbook rebuild is disabled (rebuild.enabled=false — enable it in the dsh settings panel)");
|
|
7599
|
+
const { root, note, summary, dshSessionId } = opts;
|
|
7600
|
+
const session = ctx.sessions?.get?.(dshSessionId);
|
|
7601
|
+
if (!session) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
|
|
7602
|
+
const mdPath = resolveSessionMdPath(root, dshSessionId, session);
|
|
7603
|
+
if (!mdPath) throw new Error("Unable to determine the active SESSION.md — no session context found in this conversation. Run \"logbook use <S###> --summary <内容概括 ≤20 字>\" first to activate the trajectory to resume, then retry logbook rebuild.");
|
|
7604
|
+
const sessionName = getActiveSessionInfo(dshSessionId)?.sessionId ?? sessionNameFromMdPath(mdPath);
|
|
7605
|
+
const focus = note && note.trim() !== "" ? note : void 0;
|
|
7606
|
+
const anchor = buildRebuildAnchor(root, sessionName, mdPath, DEFAULT_ANCHOR_MESSAGES, focus);
|
|
7607
|
+
const boundRec = {
|
|
7608
|
+
dirName: basename(dirname(mdPath)),
|
|
7609
|
+
mdPath,
|
|
7610
|
+
sessionId: sessionName.startsWith("S") ? sessionName : void 0,
|
|
7611
|
+
note: "rebuild queued"
|
|
7612
|
+
};
|
|
7613
|
+
appendBound(session, "rebuild", boundRec);
|
|
7614
|
+
pendingRebuilds.set(dshSessionId, {
|
|
7615
|
+
anchor,
|
|
7616
|
+
summary,
|
|
7617
|
+
mdPath,
|
|
7618
|
+
focus,
|
|
7619
|
+
queuedAt: Date.now()
|
|
7620
|
+
});
|
|
7621
|
+
writeRebuildDiag(root, {
|
|
7622
|
+
sessionId: dshSessionId,
|
|
7623
|
+
event: "queued"
|
|
7624
|
+
});
|
|
8235
7625
|
return {
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
}
|
|
7626
|
+
queued: true,
|
|
7627
|
+
anchor,
|
|
7628
|
+
sessionMdPath: mdPath
|
|
8240
7629
|
};
|
|
8241
7630
|
}
|
|
8242
7631
|
/**
|
|
8243
|
-
*
|
|
8244
|
-
*
|
|
8245
|
-
*
|
|
8246
|
-
* → provider 返回 undefined → 反代不注入(旧形态无 BrowserAuth,天然兼容)。
|
|
7632
|
+
* 执行真正的 surface replace(v1.22.4 定稿语义第二步,turn-stopping 时调用):
|
|
7633
|
+
* 当前 turn 已完整结束(所有 tool/result 已 append)→ 全部节点替换为锚点消息。
|
|
7634
|
+
* 同一会话 id 原地重建——同工作区天然满足,无新/旧会话之分。
|
|
8247
7635
|
*
|
|
8248
|
-
* v1.
|
|
8249
|
-
*
|
|
8250
|
-
*
|
|
7636
|
+
* **shadow-price 协议(v1.23.5 修复,S142 用户问询)**:DSH token-meter 的
|
|
7637
|
+
* `foldSurfaceProjection` 对 surface `replace` 要求**紧邻其前的 metering 事件**
|
|
7638
|
+
* (`compaction/summary` 或 `compaction/prune`)声明被替换范围的启发式 token 价,
|
|
7639
|
+
* 否则 claim 缺失 → `deltaTokens=0` → `contextBreakdown.messageTokens` 永不扣减
|
|
7640
|
+
* → UI「对话消息」虚高累计、往复 rebuild 计量爆炸(用户截图实证:总 15% 但
|
|
7641
|
+
* 对话消息 ~1M)。官方先例 = `compaction-tool-result-pruner`(append
|
|
7642
|
+
* `compaction/prune` → 紧接 replace)。此处同款:先 append `compaction/prune`
|
|
7643
|
+
* (定价全部被替换节点,经 ctx.tokenMeter.estimateMessage 逐节点累加),再
|
|
7644
|
+
* append 锚点 replace——meter 不可用时退化(无 claim,仅计量漂移,会话功能不受影响)。
|
|
7645
|
+
*
|
|
7646
|
+
* @returns 是否执行了 replace(false = 无 pending 或 surface 空)
|
|
8251
7647
|
*/
|
|
8252
|
-
function
|
|
8253
|
-
const
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
7648
|
+
function performRebuild(session, pending, meter) {
|
|
7649
|
+
const nodes = [...session.surface.nodes];
|
|
7650
|
+
if (nodes.length === 0) return false;
|
|
7651
|
+
if (meter) {
|
|
7652
|
+
let shadowedTokenCount = 0;
|
|
7653
|
+
const events = sessionEvents(session);
|
|
7654
|
+
for (const seq of nodes) {
|
|
7655
|
+
const event = events[seq];
|
|
7656
|
+
if (!event) continue;
|
|
7657
|
+
const message = deriveEventMessage(event);
|
|
7658
|
+
if (message) shadowedTokenCount += meter.estimateMessage(message);
|
|
7659
|
+
}
|
|
7660
|
+
session.append("compaction/prune", {
|
|
7661
|
+
shadowedRange: {
|
|
7662
|
+
start: nodes[0],
|
|
7663
|
+
end: nodes[nodes.length - 1]
|
|
7664
|
+
},
|
|
7665
|
+
shadowedSeqs: nodes,
|
|
7666
|
+
shadowedTokenCount
|
|
7667
|
+
});
|
|
7668
|
+
}
|
|
7669
|
+
session.append("user/message", {
|
|
7670
|
+
content: [{
|
|
7671
|
+
type: "text",
|
|
7672
|
+
text: pending.anchor
|
|
7673
|
+
}],
|
|
7674
|
+
source: { kind: "user" }
|
|
7675
|
+
}, {
|
|
7676
|
+
surfaceOp: {
|
|
7677
|
+
op: "replace",
|
|
7678
|
+
start: nodes[0],
|
|
7679
|
+
end: nodes[nodes.length - 1]
|
|
7680
|
+
},
|
|
7681
|
+
sourceEventSeqs: nodes
|
|
7682
|
+
});
|
|
7683
|
+
return true;
|
|
7684
|
+
}
|
|
7685
|
+
/**
|
|
7686
|
+
* 重建后重命名 dsh 会话标题(需求② S142 用户拍板:rebuild 后标题带新阶段概括)。
|
|
7687
|
+
* 标题 = S###-YYYY-MM-DD-<summary>——S### 与日期从持久轨迹目录名派生(不信任 LLM),
|
|
7688
|
+
* 概括来自 queueRebuild 的 summary 参数(服务端清洗截断)。
|
|
7689
|
+
* 失败仅 warn 不阻断重建主流程(标题美观非关键路径)。
|
|
7690
|
+
*/
|
|
7691
|
+
function renameAfterRebuild(ctx, agent, pending) {
|
|
7692
|
+
try {
|
|
7693
|
+
const mdPath = pending.mdPath;
|
|
7694
|
+
const dirName = basename(dirname(mdPath));
|
|
7695
|
+
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
7696
|
+
const title = namingTitleFor({
|
|
7697
|
+
sessionId: idMatch ? `S${idMatch[1]}` : dirName.replace(/^\d{4}-\d{2}-\d{2}--/, ""),
|
|
7698
|
+
dirName,
|
|
7699
|
+
mdPath
|
|
7700
|
+
}, pending.summary);
|
|
7701
|
+
const titles = ctx.get?.("sessionTitle");
|
|
7702
|
+
if (!titles || typeof titles.rename !== "function") {
|
|
7703
|
+
console.warn("[serenity-hooks] rebuild 后重命名跳过: sessionTitle 服务不可用");
|
|
8274
7704
|
return;
|
|
8275
7705
|
}
|
|
8276
|
-
|
|
7706
|
+
titles.rename(agent.session, title);
|
|
7707
|
+
console.log(`[serenity-hooks] rebuild 后会话重命名: ${agent.id} → ${title}`);
|
|
7708
|
+
} catch (error) {
|
|
7709
|
+
console.warn(`[serenity-hooks] rebuild 后重命名失败(不阻断): ${String(error?.message ?? error)}`);
|
|
7710
|
+
}
|
|
8277
7711
|
}
|
|
8278
|
-
/**
|
|
8279
|
-
*
|
|
8280
|
-
*
|
|
8281
|
-
*
|
|
8282
|
-
*
|
|
8283
|
-
*
|
|
7712
|
+
/**
|
|
7713
|
+
* 注册 turn-stopping 钩子(index.ts apply 调用):
|
|
7714
|
+
* agent 每轮 turn 结束前(serial)检查 pending 队列——有该会话的重建请求 → 执行清空
|
|
7715
|
+
* 并 **steer 自动继续**(v1.22.5:next-step 非空 → turn 不 break → 模型自动读取
|
|
7716
|
+
* SESSION.md 继续,无需用户手工输入;DSH 官方先例 hooks-claude-code Stop hook)。
|
|
7717
|
+
* 陈旧队列(超 TTL)丢弃防误清空;无 pending 零开销。
|
|
8284
7718
|
*/
|
|
8285
|
-
function
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
}
|
|
8301
|
-
if (!enabled) {
|
|
8302
|
-
console.log("[serenity-hooks] gateway 已停止(gatewayEnabled=false,可在 dsh 设置面板开启)");
|
|
8303
|
-
return;
|
|
8304
|
-
}
|
|
8305
|
-
current = startGateway({
|
|
8306
|
-
host: settings.gateway.host,
|
|
8307
|
-
port: settings.gateway.port,
|
|
8308
|
-
cookieName: "serenity_session",
|
|
8309
|
-
mainPort: webServer.port,
|
|
8310
|
-
loginDelayMs: 0,
|
|
8311
|
-
allowWorkspaces: settings.gateway.workspaces,
|
|
8312
|
-
cookieSecure: settings.gateway.cookieSecure === true,
|
|
8313
|
-
allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate !== false,
|
|
8314
|
-
totpEnabled: settings.gateway.totpEnabled === true
|
|
8315
|
-
}, () => readAdvancedSettings().gateway.accounts, buildDshCookieProvider(ctx, webServer.port));
|
|
8316
|
-
const accounts = settings.gateway.accounts.length;
|
|
8317
|
-
const wsNote = settings.gateway.workspaces.length === 0 ? "" : `;工作区白名单 ${settings.gateway.workspaces.length} 条`;
|
|
8318
|
-
console.log(`[serenity-hooks] gateway 已启动: http://${settings.gateway.host}:${settings.gateway.port} → 127.0.0.1:${webServer.port}` + (accounts === 0 ? "(⚠️ 未配置账号,登录页将提示)" : `(${accounts} 个账号)`) + wsNote);
|
|
8319
|
-
} catch (err) {
|
|
8320
|
-
console.warn(`[serenity-hooks] gateway 同步失败: ${String(err?.message ?? err)}`);
|
|
7719
|
+
function registerRebuildTurnHook(ctx) {
|
|
7720
|
+
ctx.on("agent/turn-stopping", (payload) => {
|
|
7721
|
+
const agent = payload?.agent;
|
|
7722
|
+
if (!agent) return;
|
|
7723
|
+
const id = agent.id;
|
|
7724
|
+
const pending = pendingRebuilds.get(id);
|
|
7725
|
+
if (pending === void 0) return;
|
|
7726
|
+
if (Date.now() - pending.queuedAt > PENDING_TTL_MS) {
|
|
7727
|
+
pendingRebuilds.delete(id);
|
|
7728
|
+
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
7729
|
+
sessionId: id,
|
|
7730
|
+
event: "ttl-dropped",
|
|
7731
|
+
detail: `queuedAt=${new Date(pending.queuedAt).toISOString()} older than ${PENDING_TTL_MS / 6e4}min TTL — turn did not end in time`
|
|
7732
|
+
});
|
|
7733
|
+
return;
|
|
8321
7734
|
}
|
|
8322
|
-
|
|
8323
|
-
sync();
|
|
8324
|
-
ctx.on("agent/session-start", (payload) => {
|
|
7735
|
+
pendingRebuilds.delete(id);
|
|
8325
7736
|
try {
|
|
8326
|
-
const
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
7737
|
+
const tokenMeter = ctx.get?.("tokenMeter");
|
|
7738
|
+
const meter = tokenMeter && typeof tokenMeter.estimateMessage === "function" ? { estimateMessage: (m) => tokenMeter.estimateMessage(m) } : void 0;
|
|
7739
|
+
if (performRebuild(agent.session, pending, meter)) {
|
|
7740
|
+
renameAfterRebuild(ctx, agent, pending);
|
|
7741
|
+
agent.steer(createUserMessage({
|
|
7742
|
+
content: [{
|
|
7743
|
+
type: "text",
|
|
7744
|
+
text: `${eventToken("rebuild")} The conversation has been cleared and rebuilt. Follow the anchor instructions above now: read the persistent trajectory (SESSION.md) and continue the work automatically from the last checkpoint.`
|
|
7745
|
+
}],
|
|
7746
|
+
source: PLUGIN_SOURCE$1
|
|
7747
|
+
}));
|
|
7748
|
+
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
7749
|
+
sessionId: id,
|
|
7750
|
+
event: "rebuilt"
|
|
7751
|
+
});
|
|
7752
|
+
console.log(`[serenity-hooks] logbook rebuild executed with auto-continue (turn ${payload.turn ?? "?"} ended): ${id}`);
|
|
7753
|
+
} else writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
7754
|
+
sessionId: id,
|
|
7755
|
+
event: "empty-surface"
|
|
7756
|
+
});
|
|
7757
|
+
} catch (error) {
|
|
7758
|
+
const msg = String(error?.message ?? error);
|
|
7759
|
+
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
7760
|
+
sessionId: id,
|
|
7761
|
+
event: "failed",
|
|
7762
|
+
detail: msg
|
|
7763
|
+
});
|
|
7764
|
+
console.warn(`[serenity-hooks] logbook rebuild failed: ${msg}`);
|
|
7765
|
+
}
|
|
8340
7766
|
});
|
|
8341
7767
|
}
|
|
7768
|
+
/** 从 agent 会话 cwd 解析 CCC 根(诊断落盘用);无则回退进程 cwd */
|
|
7769
|
+
function resolveSerenityRootFor(agent) {
|
|
7770
|
+
const cwd = agent.session?.header?.cwd;
|
|
7771
|
+
return findSerenityRoot(cwd ?? process.cwd()) ?? process.cwd();
|
|
7772
|
+
}
|
|
8342
7773
|
//#endregion
|
|
8343
7774
|
//#region src/output-guard.ts
|
|
8344
7775
|
/**
|
|
@@ -8373,7 +7804,6 @@ const MECHANISM_WORDS = [
|
|
|
8373
7804
|
"agent/turn-stopping",
|
|
8374
7805
|
"tools/pre-execute",
|
|
8375
7806
|
"tools/post-execute",
|
|
8376
|
-
"session_rebuild",
|
|
8377
7807
|
"Trajectory Steward",
|
|
8378
7808
|
"TRAJECTORY-STEWARD",
|
|
8379
7809
|
"TRAJECTORY-ASSISTANT",
|
|
@@ -9827,13 +9257,10 @@ function apply(ctx, config) {
|
|
|
9827
9257
|
ctx.tools.register(kitTool);
|
|
9828
9258
|
ctx.tools.register(gitTool);
|
|
9829
9259
|
ctx.tools.register(msmTool);
|
|
9830
|
-
ctx.tools.register(
|
|
9831
|
-
ctx.tools.register(neatTool);
|
|
9832
|
-
ctx.tools.register(cceTool);
|
|
9260
|
+
ctx.tools.register(praxisTool);
|
|
9833
9261
|
ctx.tools.register(createHandymanTool(ctx));
|
|
9834
|
-
ctx.tools.register(createRebuildTool(ctx));
|
|
9835
9262
|
ctx.tools.register(localstoreTool);
|
|
9836
|
-
ctx.tools.register(
|
|
9263
|
+
ctx.tools.register(containerAdminTool);
|
|
9837
9264
|
ctx.tools.register(createAutopilotTool(ctx));
|
|
9838
9265
|
}
|
|
9839
9266
|
if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
|