@shgroup/dsh-serenity-hooks 1.21.1 → 1.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -7,7 +7,7 @@ import { homedir, platform } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { promisify } from "node:util";
9
9
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
10
- import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
10
+ import { createHmac, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
11
11
  import { createServer, request } from "node:http";
12
12
  //#region src/ccc.ts
13
13
  /**
@@ -213,10 +213,10 @@ function runCcFs(root, args) {
213
213
  switch (a) {
214
214
  case "root": return root;
215
215
  case "resolve":
216
- if (!args.path) throw new Error("resolve 需要 path");
216
+ if (!args.path) throw new Error("resolve requires path");
217
217
  return resolveInside(root, args.path);
218
218
  case "exists": {
219
- if (!args.path) throw new Error("exists 需要 path");
219
+ if (!args.path) throw new Error("exists requires path");
220
220
  const absPath = args.path.startsWith("/") ? resolve(args.path) : resolveInside(root, args.path);
221
221
  return existsSync(absPath) ? "true" : "false";
222
222
  }
@@ -271,13 +271,13 @@ function runCcFs(root, args) {
271
271
  };
272
272
  }
273
273
  case "relative": {
274
- if (!args.path) throw new Error("relative 需要 path");
274
+ if (!args.path) throw new Error("relative requires path");
275
275
  const absPath = args.path.startsWith("/") ? resolve(args.path) : resolveInside(root, args.path);
276
276
  if (!absPath.startsWith(root)) throw new Error(`relative: path "${args.path}" resolves to "${absPath}" which is outside serenity root "${root}"`);
277
277
  return safeRel(root, absPath);
278
278
  }
279
279
  case "mkdir": {
280
- if (!args.path) throw new Error("mkdir 需要 path");
280
+ if (!args.path) throw new Error("mkdir requires path");
281
281
  const absPath = validateWritePath(root, args.path);
282
282
  if (existsSync(absPath)) {
283
283
  if (statSync(absPath).isDirectory()) return `directory already exists: ${args.path}`;
@@ -289,7 +289,7 @@ function runCcFs(root, args) {
289
289
  case "rm": {
290
290
  const targets = [...args.paths ?? []];
291
291
  if (args.path) targets.push(args.path);
292
- if (targets.length === 0) throw new Error("rm 需要至少一个 path 参数(path 或 paths)");
292
+ if (targets.length === 0) throw new Error("rm requires at least one path argument (path or paths)");
293
293
  const dryRun = args.dryRun ?? false;
294
294
  const recursive = args.recursive ?? false;
295
295
  const results = [];
@@ -334,7 +334,7 @@ function runCcFs(root, args) {
334
334
  return results.join("\n");
335
335
  }
336
336
  case "mv": {
337
- if (!args.src || !args.dst) throw new Error("mv 需要 src + dst");
337
+ if (!args.src || !args.dst) throw new Error("mv requires src + dst");
338
338
  const srcAbs = validateWritePath(root, args.src);
339
339
  const dstAbs = validateWritePath(root, args.dst);
340
340
  if (!existsSync(srcAbs)) throw new Error(`mv: source not found: ${args.src}`);
@@ -345,7 +345,7 @@ function runCcFs(root, args) {
345
345
  return `moved: ${args.src} → ${args.dst}`;
346
346
  }
347
347
  case "cp": {
348
- if (!args.src || !args.dst) throw new Error("cp 需要 src + dst");
348
+ if (!args.src || !args.dst) throw new Error("cp requires src + dst");
349
349
  const srcAbs = validateWritePath(root, args.src);
350
350
  const dstAbs = validateWritePath(root, args.dst);
351
351
  if (!existsSync(srcAbs)) throw new Error(`cp: source not found: ${args.src}`);
@@ -357,7 +357,7 @@ function runCcFs(root, args) {
357
357
  return `copied: ${args.src} → ${args.dst}`;
358
358
  }
359
359
  case "touch": {
360
- if (!args.path) throw new Error("touch 需要 path");
360
+ if (!args.path) throw new Error("touch requires path");
361
361
  const absPath = validateWritePath(root, args.path);
362
362
  if (existsSync(absPath)) {
363
363
  const now = /* @__PURE__ */ new Date();
@@ -370,7 +370,7 @@ function runCcFs(root, args) {
370
370
  return `created empty file: ${args.path}`;
371
371
  }
372
372
  case "append": {
373
- if (!args.path || args.content === void 0) throw new Error("append 需要 path + content");
373
+ if (!args.path || args.content === void 0) throw new Error("append requires path + content");
374
374
  const absPath = validateWritePath(root, args.path);
375
375
  const parentDir = dirname(absPath);
376
376
  if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
@@ -379,7 +379,7 @@ function runCcFs(root, args) {
379
379
  return `appended ${Buffer.byteLength(content, "utf8")} bytes to ${args.path}`;
380
380
  }
381
381
  case "reveal": {
382
- if (!args.path) throw new Error("reveal 需要 path");
382
+ if (!args.path) throw new Error("reveal requires path");
383
383
  const absPath = resolveInside(root, args.path);
384
384
  if (!existsSync(absPath)) throw new Error(`no such path: ${absPath}`);
385
385
  const os = platform();
@@ -405,7 +405,7 @@ function runCcFs(root, args) {
405
405
  }
406
406
  }
407
407
  case "info": {
408
- if (!args.path) throw new Error("info 需要 path");
408
+ if (!args.path) throw new Error("info requires path");
409
409
  const absPath = args.path.startsWith("/") ? resolve(args.path) : resolveInside(root, args.path);
410
410
  if (!existsSync(absPath)) throw new Error(`info: path "${absPath}" does not exist`);
411
411
  const stat = statSync(absPath);
@@ -422,7 +422,7 @@ function runCcFs(root, args) {
422
422
  ].join("\n");
423
423
  }
424
424
  case "find": {
425
- if (!args.pattern) throw new Error("find 需要 pattern");
425
+ if (!args.pattern) throw new Error("find requires pattern");
426
426
  const relPath = args.path || ".";
427
427
  const absPath = relPath.startsWith("/") ? resolve(relPath) : resolveInside(root, relPath);
428
428
  if (!existsSync(absPath)) throw new Error(`find: path "${absPath}" does not exist`);
@@ -471,7 +471,7 @@ function runCcFs(root, args) {
471
471
  count: matches.length
472
472
  };
473
473
  }
474
- default: throw new Error(`未知 action: ${a}`);
474
+ default: throw new Error(`Unknown action: ${a}`);
475
475
  }
476
476
  }
477
477
  //#endregion
@@ -493,66 +493,66 @@ function renderText$10(value) {
493
493
  }
494
494
  const ccFsTool = defineTool({
495
495
  name: "cc_fs",
496
- description: "CCC 内文件系统操作(cc-fs 语义,DSH 原生版)。15 子命令:root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find。全部路径限定在 CCC 根内,路径逃逸自动阻断。reveal 在 OS 文件管理器中打开路径(Linux xdg-open / macOS Finder / Windows Explorer)。与 read/write/edit 互补(结构性操作)。",
496
+ description: "CCC filesystem operations (cc-fs semantics, DSH-native). 15 subcommands: root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find. All paths confined to the CCC root; path escape is blocked automatically. reveal opens a path in the OS file manager (Linux xdg-open / macOS Finder / Windows Explorer). Complements read/write/edit (structural operations).",
497
497
  parameters: {
498
498
  action: {
499
499
  type: "string",
500
500
  enum: [...CC_FS_ACTIONS],
501
501
  required: true,
502
- description: "子命令"
502
+ description: "Subcommand"
503
503
  },
504
504
  path: {
505
505
  type: "string",
506
- description: "主路径参数(resolve/exists/list/tree/relative/touch/append/info/rm 单路径)"
506
+ description: "Primary path argument (single-path ops: resolve/exists/list/tree/relative/touch/append/info/rm)"
507
507
  },
508
508
  paths: {
509
509
  type: "array",
510
510
  items: { type: "string" },
511
- description: "批量路径(mkdir/rm)"
511
+ description: "Batch paths (mkdir/rm)"
512
512
  },
513
513
  src: {
514
514
  type: "string",
515
- description: "mv/cp 源路径"
515
+ description: "mv/cp source path"
516
516
  },
517
517
  dst: {
518
518
  type: "string",
519
- description: "mv/cp 目标路径"
519
+ description: "mv/cp destination path"
520
520
  },
521
521
  content: {
522
522
  type: "string",
523
- description: "append 内容"
523
+ description: "append content"
524
524
  },
525
525
  pattern: {
526
526
  type: "string",
527
- description: "find 匹配(glob *? 或大小写不敏感子串)"
527
+ description: "find match (glob *? or case-insensitive substring)"
528
528
  },
529
529
  depth: {
530
530
  type: "integer",
531
- description: "tree 最大深度(1-10,默认 3)"
531
+ description: "tree max depth (1-10, default 3)"
532
532
  },
533
533
  dryRun: {
534
534
  type: "boolean",
535
- description: "rm/archive 预览模式"
535
+ description: "rm/archive preview mode"
536
536
  },
537
537
  recursive: {
538
538
  type: "boolean",
539
- description: "rm 删目录 / cp 复制目录需 recursive"
539
+ description: "rm directory / cp directory requires recursive"
540
540
  },
541
541
  filesOnly: {
542
542
  type: "boolean",
543
- description: "tree 只显示文件(与 dirsOnly 互斥)"
543
+ description: "tree shows files only (mutually exclusive with dirsOnly)"
544
544
  },
545
545
  dirsOnly: {
546
546
  type: "boolean",
547
- description: "tree 只显示目录(与 filesOnly 互斥)"
547
+ description: "tree shows directories only (mutually exclusive with filesOnly)"
548
548
  },
549
549
  absolute: {
550
550
  type: "boolean",
551
- description: "find 返回绝对路径"
551
+ description: "find returns absolute paths"
552
552
  },
553
553
  maxDepth: {
554
554
  type: "integer",
555
- description: "find 最大递归深度(缺省不限)"
555
+ description: "find max recursion depth (default unlimited)"
556
556
  }
557
557
  },
558
558
  output: {
@@ -625,24 +625,24 @@ function isWriteTool(toolName, action) {
625
625
  function decideGuard(input) {
626
626
  const { root, toolName, safeModeOn, blacklist, pathArg, action } = input;
627
627
  if (safeModeOn && toolName === "bash") return {
628
- deny: `bash: 没有这个工具`,
628
+ deny: `bash: no such tool`,
629
629
  kind: "deny"
630
630
  };
631
631
  if (pathArg !== void 0) {
632
632
  const abs = resolve(root, pathArg);
633
633
  if (!pathInside(resolve(root), abs)) return {
634
- deny: `path escape blocked: "${pathArg}" 越出 CCC 根`,
634
+ deny: `path escape blocked: "${pathArg}" escapes the CCC root`,
635
635
  kind: "deny"
636
636
  };
637
637
  if (isWriteTool(toolName, action)) {
638
638
  const rel = relative(root, abs).split("\\").join("/");
639
639
  if (rel === ".serenity-safe-on" || rel === ".serenity" || rel.startsWith(".serenity-safe-on/") || rel.startsWith(".serenity/")) return {
640
- deny: `CCC 治理文件 "${rel}" 保留给用户,agent 不可写`,
640
+ deny: `CCC governance file "${rel}" is reserved for the user — agent must not write`,
641
641
  kind: "deny"
642
642
  };
643
643
  const hit = matchBlacklist(rel, blacklist);
644
644
  if (hit) return {
645
- deny: hit.message ?? `blacklist blocked: "${pathArg}" 命中规则 "${hit.pattern}"`,
645
+ deny: hit.message ?? `blacklist blocked: "${pathArg}" matches rule "${hit.pattern}"`,
646
646
  kind: "deny"
647
647
  };
648
648
  }
@@ -916,11 +916,11 @@ async function runKit(root, args) {
916
916
  }
917
917
  case "wait": {
918
918
  const seconds = args.seconds ?? 1;
919
- if (!Number.isInteger(seconds) || seconds <= 0) throw new Error("wait 需要正整数秒数(缺省 1)");
919
+ if (!Number.isInteger(seconds) || seconds <= 0) throw new Error("wait requires a positive integer number of seconds (default 1)");
920
920
  await new Promise((r) => setTimeout(r, seconds * 1e3));
921
921
  return `waited ${seconds}s`;
922
922
  }
923
- default: throw new Error(`未知 action: ${args.action}`);
923
+ default: throw new Error(`Unknown action: ${args.action}`);
924
924
  }
925
925
  }
926
926
  //#endregion
@@ -939,17 +939,17 @@ function renderText$9(value) {
939
939
  }
940
940
  const kitTool = defineTool({
941
941
  name: "acc_kit",
942
- description: "ACC 通用能力工具包:health(CCC 三原则检查 P1/P2/配置,healthy/degraded 报告)/ time(now_iso/now_local/epoch_ms)/ wait(等待 N 秒,缺省 1)。进入 CCC 工作前的例行自检。",
942
+ description: "ACC general-purpose utility kit: health (CCC three-principle check P1/P2/config, healthy/degraded report) / time (now_iso/now_local/epoch_ms) / wait (wait N seconds, default 1). Routine self-check before entering a CCC.",
943
943
  parameters: {
944
944
  action: {
945
945
  type: "string",
946
946
  enum: [...KIT_ACTIONS],
947
947
  required: true,
948
- description: "子命令"
948
+ description: "Subcommand"
949
949
  },
950
950
  seconds: {
951
951
  type: "integer",
952
- description: "wait 的秒数(正整数,缺省 1)"
952
+ description: "Seconds to wait (positive integer, default 1)"
953
953
  }
954
954
  },
955
955
  output: {
@@ -1018,7 +1018,7 @@ function ensureLocalstoreGitignored(root) {
1018
1018
  if (isLocalstoreGitignored(root)) return { status: "ignored" };
1019
1019
  const gi = join(root, ".gitignore");
1020
1020
  const existing = existsSync(gi) ? readFileSync(gi, "utf-8") : "";
1021
- const line = `${LOCALSTORE_FILENAME} # ACC localstore(localstore.gitTrack 缺省 deny,禁提交)`;
1021
+ const line = `${LOCALSTORE_FILENAME} # ACC localstore (localstore.gitTrack defaults to deny — do not commit)`;
1022
1022
  writeFileSync(gi, (existing.endsWith("\n") || existing === "" ? existing : existing + "\n") + line + "\n", "utf-8");
1023
1023
  return { status: "appended" };
1024
1024
  }
@@ -1032,11 +1032,11 @@ function checkLocalstoreGitCompliance(root) {
1032
1032
  if (isLocalstoreGitignored(root)) return { ok: true };
1033
1033
  return {
1034
1034
  ok: false,
1035
- reason: `localstore.json 禁止提交(localstore.gitTrack=deny 缺省)但 .gitignore 未包含——请将 ${LOCALSTORE_FILENAME} 加入 .gitignore(或改配置 localstore.gitTrack=allow 显式放行)`
1035
+ reason: `localstore.json must not be committed (localstore.gitTrack=deny default) but .gitignore does not cover it — add ${LOCALSTORE_FILENAME} to .gitignore (or set localstore.gitTrack=allow to explicitly permit)`
1036
1036
  };
1037
1037
  }
1038
1038
  /** 读取全文件(顶层分节);文件不存在/坏 JSON 返回空 */
1039
- function readAll$1(root) {
1039
+ function readAll(root) {
1040
1040
  const p = localstorePath(root);
1041
1041
  if (!existsSync(p)) return {};
1042
1042
  try {
@@ -1048,28 +1048,28 @@ function readAll$1(root) {
1048
1048
  }
1049
1049
  }
1050
1050
  /** 写回全文件(2 空格缩进 + 尾换行,方便 MSM 直接读取) */
1051
- function writeAll$1(root, data) {
1051
+ function writeAll(root, data) {
1052
1052
  writeFileSync(localstorePath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
1053
1053
  }
1054
1054
  /** 读取命名空间全部条目:credential → 扁平;config → 分节(剔除 credentials 保留节) */
1055
1055
  function readStore(root, scope) {
1056
- const all = readAll$1(root);
1056
+ const all = readAll(root);
1057
1057
  if (scope === "credential") return all["credentials"] ?? {};
1058
1058
  const { [CREDENTIALS_SECTION]: _cred, ...rest } = all;
1059
1059
  return rest;
1060
1060
  }
1061
1061
  /** 校验凭据 key 合法(大写蛇形);不合法抛错 */
1062
1062
  function assertCredentialKey(key) {
1063
- if (!CREDENTIAL_KEY_RE.test(key)) throw new Error(`credential key "${key}" 必须匹配大写蛇形 ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)`);
1063
+ if (!CREDENTIAL_KEY_RE.test(key)) throw new Error(`credential key "${key}" must match UPPER_SNAKE ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)`);
1064
1064
  }
1065
1065
  /** 校验 config 路径(节.key);不合法抛错 */
1066
1066
  function assertConfigPath(path) {
1067
1067
  const idx = path.indexOf(".");
1068
- if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" 必须为 section.key(如 loop.defaultModel)`);
1068
+ if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" must be section.key (e.g. loop.defaultModel)`);
1069
1069
  const section = path.slice(0, idx);
1070
1070
  const key = path.slice(idx + 1);
1071
- if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config 节 "${section}" 必须匹配 ^[a-z][a-z0-9-]*$`);
1072
- if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" 必须匹配小驼峰 ^[a-z][a-zA-Z0-9_]*$(如 defaultModel)`);
1071
+ if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config section "${section}" must match ^[a-z][a-z0-9-]*$`);
1072
+ if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" must match lowerCamel ^[a-z][a-zA-Z0-9_]*$ (e.g. defaultModel)`);
1073
1073
  return {
1074
1074
  section,
1075
1075
  key
@@ -1077,7 +1077,7 @@ function assertConfigPath(path) {
1077
1077
  }
1078
1078
  /** 写入单个条目(自动建文件;deny 默认时同步确保 .gitignore 物理保证) */
1079
1079
  function writeEntry(root, scope, name, value) {
1080
- const all = readAll$1(root);
1080
+ const all = readAll(root);
1081
1081
  if (scope === "credential") {
1082
1082
  assertCredentialKey(name);
1083
1083
  all[CREDENTIALS_SECTION] ??= {};
@@ -1087,12 +1087,12 @@ function writeEntry(root, scope, name, value) {
1087
1087
  all[section] ??= {};
1088
1088
  all[section][key] = value;
1089
1089
  }
1090
- writeAll$1(root, all);
1090
+ writeAll(root, all);
1091
1091
  ensureLocalstoreGitignored(root);
1092
1092
  }
1093
1093
  /** 删除单个条目(不存在返回 false);空节自动移除 */
1094
1094
  function unsetEntry(root, scope, name) {
1095
- const all = readAll$1(root);
1095
+ const all = readAll(root);
1096
1096
  if (scope === "credential") {
1097
1097
  assertCredentialKey(name);
1098
1098
  const sec = all[CREDENTIALS_SECTION];
@@ -1106,12 +1106,12 @@ function unsetEntry(root, scope, name) {
1106
1106
  delete sec[key];
1107
1107
  if (Object.keys(sec).length === 0) delete all[section];
1108
1108
  }
1109
- writeAll$1(root, all);
1109
+ writeAll(root, all);
1110
1110
  return true;
1111
1111
  }
1112
1112
  /** 读取单个条目值;不存在返回 null */
1113
1113
  function getEntry(root, scope, name) {
1114
- const all = readAll$1(root);
1114
+ const all = readAll(root);
1115
1115
  if (scope === "credential") {
1116
1116
  assertCredentialKey(name);
1117
1117
  return all["credentials"]?.[name] ?? null;
@@ -1132,18 +1132,18 @@ function listKeys(root, scope) {
1132
1132
  function docText(root) {
1133
1133
  const path = localstorePath(root);
1134
1134
  return [
1135
- "# localstore — ACC 本地凭据/配置存储(标准,S134 重设计)",
1135
+ "# localstore — ACC local credential/config storage (standard, S134 redesign)",
1136
1136
  "",
1137
- "一个工具管理两个命名空间:credential(凭据)+ config(偏好)。",
1138
- `存储于 CCC 根根目录 ${path}(JSON 格式,MSM 可直接 read + JSON.parse)。`,
1137
+ "One tool manages two namespaces: credential (credentials) + config (preferences).",
1138
+ `Stored at the CCC root in ${path} (JSON format; MSMs can read + JSON.parse directly).`,
1139
1139
  "",
1140
- "## git 提交策略",
1141
- "- 配置:.opencode/serenity.json `localstore.gitTrack`:`\"allow\"`(可提交)| `\"deny\"`(禁提交;.dsh 回退)",
1142
- "- **缺省 deny(没配就是不提交)**;deny 时写入会自动确保 .gitignore 含 localstore.json",
1143
- " (物理保证,不依赖 dsh 运行);cc_git commit 会检查拒绝",
1144
- "- 想提交:配置 `\"localstore\": { \"gitTrack\": \"allow\" }`,并从 .gitignore 移除 localstore.json",
1140
+ "## Git commit policy",
1141
+ "- Config: .opencode/serenity.json `localstore.gitTrack`: `\"allow\"` (may commit) | `\"deny\"` (must not commit; .dsh fallback)",
1142
+ "- **Default deny (unset = not committed)**; when deny, writes automatically ensure .gitignore contains localstore.json",
1143
+ " (physical guarantee, independent of dsh runtime); cc_git commit checks and refuses",
1144
+ "- To commit: set `\"localstore\": { \"gitTrack\": \"allow\" }` and remove localstore.json from .gitignore",
1145
1145
  "",
1146
- "## 格式(JSON 顶层分节)",
1146
+ "## Format (JSON top-level sections)",
1147
1147
  "```json",
1148
1148
  "{",
1149
1149
  " \"credentials\": {",
@@ -1154,24 +1154,24 @@ function docText(root) {
1154
1154
  " }",
1155
1155
  "}",
1156
1156
  "```",
1157
- "- credentials 为保留节(凭据,key 大写蛇形);其余节 = config(path = section.key)",
1157
+ "- credentials is a reserved section (credentials, UPPER_SNAKE keys); other sections = config (path = section.key)",
1158
1158
  "",
1159
- "## key 规范",
1160
- "- credential key: ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)",
1161
- "- config path: section.key(节 ^[a-z][a-z0-9-]*$,key 小驼峰 ^[a-z][a-zA-Z0-9_]*$,如 loop.defaultModel)",
1159
+ "## Key conventions",
1160
+ "- credential key: ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)",
1161
+ "- config path: section.key (section ^[a-z][a-z0-9-]*$, key lowerCamel ^[a-z][a-zA-Z0-9_]*$, e.g. loop.defaultModel)",
1162
1162
  "",
1163
- "## 读取方法(agent 可直接用 fs 工具)",
1164
- `- 用 read 工具读 ${path} → JSON.parse 解析`,
1165
- "- 或用 localstore get <name> [--scope credential|config]",
1163
+ "## Reading (agents may use fs tools directly)",
1164
+ `- Read ${path} with the read tool → JSON.parse`,
1165
+ "- Or use localstore get <name> [--scope credential|config]",
1166
1166
  "",
1167
- "## 写入方法",
1168
- "- 推荐:localstore set <name> <value> [--scope ...](自动建文件/保留其他条目/联动 .gitignore)",
1169
- "- 或直接用 write/edit 工具修改文件,保持 JSON 合法",
1167
+ "## Writing",
1168
+ "- Recommended: localstore set <name> <value> [--scope ...] (auto-creates file / preserves other entries / syncs .gitignore)",
1169
+ "- Or modify the file directly with write/edit, keeping the JSON valid",
1170
1170
  "",
1171
- "## 安全边界",
1172
- "- list/show 对 credential 只返回 key 名,不返回值",
1173
- "- 凭据值应在 agent 内部使用,不要写入对话/日志",
1174
- "- 默认 deny:文件不会提交 git(.gitignore 物理保证 + cc_git 检查兜底)",
1171
+ "## Security boundary",
1172
+ "- list/show return only key names for credentials, never values",
1173
+ "- Credential values should be used internally by the agent; never write them into conversation or logs",
1174
+ "- Default deny: the file is not committed to git (.gitignore physical guarantee + cc_git check fallback)",
1175
1175
  ""
1176
1176
  ].join("\n");
1177
1177
  }
@@ -1184,9 +1184,9 @@ function runLocalStore(root, args) {
1184
1184
  keys: listKeys(root, scope)
1185
1185
  };
1186
1186
  case "get": {
1187
- if (!args.name) throw new Error("get 需要 name");
1187
+ if (!args.name) throw new Error("get requires name");
1188
1188
  const value = getEntry(root, scope, args.name);
1189
- if (value === null) throw new Error(`not found: ${args.name}(scope=${scope})`);
1189
+ if (value === null) throw new Error(`not found: ${args.name} (scope=${scope})`);
1190
1190
  return {
1191
1191
  scope,
1192
1192
  name: args.name,
@@ -1195,8 +1195,8 @@ function runLocalStore(root, args) {
1195
1195
  };
1196
1196
  }
1197
1197
  case "set": {
1198
- if (!args.name) throw new Error("set 需要 name");
1199
- if (args.value === void 0) throw new Error("set 需要 value");
1198
+ if (!args.name) throw new Error("set requires name");
1199
+ if (args.value === void 0) throw new Error("set requires value");
1200
1200
  writeEntry(root, scope, args.name, args.value);
1201
1201
  const git = checkLocalstoreGitCompliance(root);
1202
1202
  return {
@@ -1210,7 +1210,7 @@ function runLocalStore(root, args) {
1210
1210
  };
1211
1211
  }
1212
1212
  case "unset": {
1213
- if (!args.name) throw new Error("unset 需要 name");
1213
+ if (!args.name) throw new Error("unset requires name");
1214
1214
  const removed = unsetEntry(root, scope, args.name);
1215
1215
  return {
1216
1216
  scope,
@@ -1219,7 +1219,7 @@ function runLocalStore(root, args) {
1219
1219
  };
1220
1220
  }
1221
1221
  case "show": {
1222
- if (!args.name) throw new Error("show 需要 name");
1222
+ if (!args.name) throw new Error("show requires name");
1223
1223
  const exists = getEntry(root, scope, args.name) !== null;
1224
1224
  return {
1225
1225
  scope,
@@ -1229,7 +1229,7 @@ function runLocalStore(root, args) {
1229
1229
  };
1230
1230
  }
1231
1231
  case "doc": return { doc: docText(root) };
1232
- default: throw new Error(`未知子命令: ${args.action}(可用 list/get/set/unset/show/doc)`);
1232
+ default: throw new Error(`Unknown subcommand: ${args.action} (available: list/get/set/unset/show/doc)`);
1233
1233
  }
1234
1234
  }
1235
1235
  //#endregion
@@ -1276,7 +1276,7 @@ function git(root, args) {
1276
1276
  } catch (err) {
1277
1277
  return {
1278
1278
  stdout: (err.stdout?.toString() ?? "").trimEnd(),
1279
- stderr: err.killed ? `git 操作超时(${GIT_TIMEOUT_MS / 1e3}s)` : (err.stderr?.toString() ?? "").trimEnd()
1279
+ stderr: err.killed ? `git operation timed out (${GIT_TIMEOUT_MS / 1e3}s)` : (err.stderr?.toString() ?? "").trimEnd()
1280
1280
  };
1281
1281
  }
1282
1282
  }
@@ -1306,7 +1306,7 @@ function runGit(root, args) {
1306
1306
  "status",
1307
1307
  "--porcelain"
1308
1308
  ]);
1309
- if (r.stderr) throw new Error(`status 失败:${r.stderr.trim()}`);
1309
+ if (r.stderr) throw new Error(`status failed: ${r.stderr.trim()}`);
1310
1310
  const files = (r.stdout ? r.stdout.split("\n") : []).map((line) => {
1311
1311
  return {
1312
1312
  status: line.slice(0, 2).trim() || "??",
@@ -1353,7 +1353,7 @@ function runGit(root, args) {
1353
1353
  "origin",
1354
1354
  branch
1355
1355
  ]);
1356
- if (stderr.includes("non-fast-forward") || stderr.includes("rejected") || stderr.includes("[rejected]")) return `[REJECTED] Push to origin/${branch} was rejected (non-fast-forward).\n\n远程有新的提交,本地落后。操作建议:\n 1. 先用 bash: git fetch origin ${branch}\n 2. 查看远程变更: git log HEAD..origin/${branch}\n 3. 合并或变基: git merge origin/${branch} 或 git rebase origin/${branch}\n 4. 有冲突则手动解决后: git add ... && git commit\n 5. 再次推送: cc_git push`;
1356
+ 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: cc_git push`;
1357
1357
  if (!stderr || stderr.includes("->") || stderr === "") return stdout || `Pushed to origin/${branch}`;
1358
1358
  throw new Error(`cc_git push failed:\n${stderr}`);
1359
1359
  }
@@ -1382,7 +1382,7 @@ function runGit(root, args) {
1382
1382
  const msg = mergeResult.stdout || "Pulled successfully.";
1383
1383
  return msg.endsWith("\n") ? msg.trimEnd() : msg;
1384
1384
  }
1385
- 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\n远程有新的提交,本地的历史与远程产生了分歧(非快进)。操作建议:\n 1. 查看差异: cc_git log HEAD..origin/${branch}\n 2. 用 bash 手动合并: git merge origin/${branch}\n 3. 或用 rebase: git rebase origin/${branch}\n 4. 有冲突则手动解决后: git add <file> && git commit\n 5. 推送: cc_git push`;
1385
+ 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: cc_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: cc_git push`;
1386
1386
  throw new Error(`cc_git pull failed:\n${mergeResult.stderr}`);
1387
1387
  }
1388
1388
  case "log": {
@@ -1395,7 +1395,7 @@ function runGit(root, args) {
1395
1395
  "-n",
1396
1396
  String(n)
1397
1397
  ]);
1398
- if (r.stderr) throw new Error(`git log 失败:${r.stderr.trim()}`);
1398
+ if (r.stderr) throw new Error(`git log failed: ${r.stderr.trim()}`);
1399
1399
  if (!r.stdout) return "(no commits)";
1400
1400
  return r.stdout;
1401
1401
  }
@@ -1409,7 +1409,7 @@ function runGit(root, args) {
1409
1409
  if (!stdout) return "(no diff)";
1410
1410
  return stdout;
1411
1411
  }
1412
- default: throw new Error(`未知 action: ${args.action}`);
1412
+ default: throw new Error(`Unknown action: ${args.action}`);
1413
1413
  }
1414
1414
  }
1415
1415
  //#endregion
@@ -1428,33 +1428,33 @@ function renderText$8(value) {
1428
1428
  }
1429
1429
  const gitTool = defineTool({
1430
1430
  name: "cc_git",
1431
- description: "CCC 内 git 操作(cc-git 语义,对齐 osp)。status/commit/push/log/pull/diff;push/pull 非快进时输出 [REJECTED] + 操作建议(绝不自动 force)。merge/rebase/冲突解决走 bash。",
1431
+ 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.",
1432
1432
  parameters: {
1433
1433
  action: {
1434
1434
  type: "string",
1435
1435
  enum: [...GIT_ACTIONS],
1436
1436
  required: true,
1437
- description: "子命令:status/commit/push/log/pull/diff"
1437
+ description: "Subcommand: status/commit/push/log/pull/diff"
1438
1438
  },
1439
1439
  message: {
1440
1440
  type: "string",
1441
- description: "commit 消息(commit 必填)"
1441
+ description: "Commit message (required for commit)"
1442
1442
  },
1443
1443
  count: {
1444
1444
  type: "integer",
1445
- description: "log 条数(默认 10,max 100;运行时 clamp 1-100)"
1445
+ description: "Log count (default 10, max 100; runtime clamp 1-100)"
1446
1446
  },
1447
1447
  staged: {
1448
1448
  type: "boolean",
1449
- description: "diff: 显示暂存区变更(--cached)"
1449
+ description: "diff: show staged changes (--cached)"
1450
1450
  },
1451
1451
  ref: {
1452
1452
  type: "string",
1453
- description: "diff: 对比 ref(如 HEAD~1 / main / origin/main)"
1453
+ description: "diff: compare ref (e.g. HEAD~1 / main / origin/main)"
1454
1454
  },
1455
1455
  path: {
1456
1456
  type: "string",
1457
- description: "diff: 限定路径(如 src/、package.json)"
1457
+ description: "diff: restrict to path (e.g. src/, package.json)"
1458
1458
  }
1459
1459
  },
1460
1460
  output: {
@@ -1491,10 +1491,10 @@ function readCccName(root) {
1491
1491
  */
1492
1492
  function assertPathInsideRoot(root, value, flagName) {
1493
1493
  const abs = resolve(root, value);
1494
- if (classifyPath(abs, root) === "outside") throw new Error(`Path escape blocked: --${flagName}=${value} 越出 CCC 根`);
1494
+ if (classifyPath(abs, root) === "outside") throw new Error(`Path escape blocked: --${flagName}=${value} escapes the CCC root`);
1495
1495
  if (existsSync(abs)) try {
1496
1496
  const real = realpathSync(abs);
1497
- if (classifyPath(real, root) === "outside") throw new Error(`Path escape blocked: --${flagName}=${value} 经 symlink 指向根外 (${real})`);
1497
+ if (classifyPath(real, root) === "outside") throw new Error(`Path escape blocked: --${flagName}=${value} resolves via symlink outside the root (${real})`);
1498
1498
  } catch (e) {
1499
1499
  if (e instanceof Error && e.message.includes("symlink")) throw e;
1500
1500
  }
@@ -1523,58 +1523,58 @@ const MSM_ACTIONS = [
1523
1523
  "guide",
1524
1524
  "ccc-config"
1525
1525
  ];
1526
- const MSM_GUIDE = `MSM 开发手册(Mech & Semi-Mech 框架)
1526
+ const MSM_GUIDE = `MSM Development Manual (Mech & Semi-Mech framework)
1527
1527
 
1528
- ## 是什么
1529
- MSM = 可执行单元层。Mech 纯 TS 零 LLM 推理;Semi-Mech TS 框架 + LLM 决策点。
1530
- MSM 是 ACC 的确定性可执行单元层——所有 shell/exec 操作走 MSM,不可绕过。
1528
+ ## What it is
1529
+ MSM = the executable-unit layer. Mech is pure TS with zero LLM reasoning; Semi-Mech is a TS framework + LLM decision points.
1530
+ MSM is ACC's deterministic executable-unit layer — all shell/exec operations go through MSM and cannot be bypassed.
1531
1531
 
1532
- ## 注册新 MSM(acc_msm register)
1533
- 1. 在 <skill>/scripts/ 写脚本(tsx 可跑;必须带 main() CLI 守卫 import.meta.url 检查)
1534
- 2. acc_msm register <name> --skill <s> --path <脚本相对根路径> --category <mech|semi-mech> --description <desc>
1535
- 3. 自动写入 mech-registry.json(保留原格式)+ git commit(只提交注册表文件)
1536
- 4. 校验:path 必须根内、脚本必须存在、name 全局唯一
1532
+ ## Registering a new MSM (acc_msm register)
1533
+ 1. Write the script under <skill>/scripts/ (runnable via tsx; must include a main() CLI guard with an import.meta.url check)
1534
+ 2. acc_msm register <name> --skill <s> --path <script path relative to root> --category <mech|semi-mech> --description <desc>
1535
+ 3. Auto-writes mech-registry.json (preserving the original format) + git commit (commits only the registry file)
1536
+ 4. Validation: path must be inside root, script must exist, name must be globally unique
1537
1537
 
1538
- ## flag schema(v1)
1539
- flags 是 new-style 对象数组,用于参数校验与 path 逃逸守卫:
1540
- [{"name":"output","type":"string","description":"输出路径"},
1541
- {"name":"target","type":"path","description":"操作目标(type:path 启用根内校验 + symlink 防御)"},
1542
- {"name":"force","type":"boolean","description":"强制模式","default":false}]
1543
- - {name, type} 格式 — new style,type:"path" 启用 path-escape 守卫
1544
- - {flag, description} 格式 — old style,CLI flag 描述字符串
1545
- - 注册时 flags 经 acc_msm register --flags '<json>' 传入(工具当前解析 name 风格)
1538
+ ## flag schema (v1)
1539
+ flags is a new-style object array for parameter validation and path-escape guarding:
1540
+ [{"name":"output","type":"string","description":"output path"},
1541
+ {"name":"target","type":"path","description":"operation target (type:path enables in-root validation + symlink defense)"},
1542
+ {"name":"force","type":"boolean","description":"force mode","default":false}]
1543
+ - {name, type} format — new style, type:"path" enables the path-escape guard
1544
+ - {flag, description} format — old style, CLI flag description string
1545
+ - On registration, flags are passed via acc_msm register --flags '<json>' (the tool currently parses the name style)
1546
1546
 
1547
- ## 脚本约定
1548
- - 顶部文档:用途/用法/退出码
1549
- - 退出码:0 成功 / 1 user / 2 system / 3 operator(对齐 ACC 协议分类)
1550
- - main() CLI 守卫(DC-M2):脚本顶部必须有
1547
+ ## Script conventions
1548
+ - Top-of-file documentation: purpose / usage / exit codes
1549
+ - Exit codes: 0 success / 1 user / 2 system / 3 operator (per ACC protocol classification)
1550
+ - main() CLI guard (DC-M2): the script must start with
1551
1551
  if (import.meta.url === \`file://\${process.argv[1]}\`) { main() }
1552
- 或等价 isMain / require.main === 判断——vitest import 时不触发顶层代码
1553
- - 配对 .test.ts 或 .spec.ts(DC-M1,vitest)
1554
- - 业务子进程环境:注入 SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION
1552
+ or an equivalent isMain / require.main === check — so vitest imports never trigger top-level code
1553
+ - A paired .test.ts or .spec.ts (DC-M1, vitest)
1554
+ - Business subprocess environment: SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION injected
1555
1555
 
1556
- ## 交互与确认规范(禁止阻塞性确认)
1557
- - MSM 运行在**无用户交互的子进程**(spawn/execFile,600s 超时)——**禁止**用
1558
- readline / prompt / process.stdin 等阻塞等待用户输入(会卡死至超时被 kill)
1559
- - 需要二次确认时:**直接返回确认信息**(输出将执行的操作与影响 + 如何带确认参数重试),
1560
- 以非 0 退出码返回(或明确提示)
1561
- - agent 确认后**重新调用 MSM 并带上确认参数**(如 --confirm / --yes / --force)重试
1562
- - 标准模式(两段式):
1563
- 1. 首次调用:检测到破坏性/需确认操作且未带确认 flag → 输出确认请求(列出操作/影响/回滚),
1564
- exit 非 0(如 1 user),**不执行任何变更**
1565
- 2. agent 评估后重新调用:带确认 flag → 执行并输出结果
1566
- - 适用场景:删除/覆盖/推送/批量操作 等不可逆或影响面大的操作
1567
- - 反例(禁止):readline 等待用户输入、process.stdin.on('data') 阻塞等待
1568
- (MSM 无 stdin 交互通道——卡死至 600s 超时)
1556
+ ## Interaction & confirmation conventions (no blocking confirmation)
1557
+ - MSMs run in **user-less subprocesses** (spawn/execFile, 600s timeout) — **never** use
1558
+ readline / prompt / process.stdin etc. to block waiting for user input (it would hang until killed by the timeout)
1559
+ - When a second confirmation is needed: **directly return the confirmation request** (list the operation to perform and its impact + how to retry with the confirmation flag),
1560
+ returning a non-zero exit code (or an explicit hint)
1561
+ - After the agent confirms, **re-invoke the MSM with the confirmation flag** (e.g. --confirm / --yes / --force) to retry
1562
+ - Standard mode (two-phase):
1563
+ 1. First call: destructive/confirmation-requiring operation detected and no confirmation flag present → output the confirmation request (list operation/impact/rollback),
1564
+ exit non-zero (e.g. 1 user), **perform no changes**
1565
+ 2. After the agent evaluates, re-invoke: with the confirmation flag → execute and output the result
1566
+ - Applies to: delete/overwrite/push/batch operations and other irreversible or wide-impact operations
1567
+ - Anti-example (forbidden): readline waiting for user input, process.stdin.on('data') blocking waits
1568
+ (MSMs have no stdin interaction channel — they hang until the 600s timeout)
1569
1569
 
1570
- ## 品质检查(acc_msm check,DC-M1~M4)
1571
- DC-M1 有 .test.ts/.spec.ts;DC-M2 有 main() 守卫(function main( / isMain / require.main === / import.meta.url);
1572
- DC-M3 双向:脚本未注册 + 注册表引用脚本缺失;DC-M4 路径型 flag 标记 type:"path"
1570
+ ## Quality checks (acc_msm check, DC-M1~M4)
1571
+ DC-M1 has .test.ts/.spec.ts; DC-M2 has a main() guard (function main( / isMain / require.main === / import.meta.url);
1572
+ DC-M3 bidirectional: scripts unregistered + registry references missing scripts; DC-M4 path-type flags marked type:"path"
1573
1573
 
1574
- ## 自描述(协议 flag,仅限参数首位)
1575
- acc_msm exec <name> --list — 列出全部 MSM
1576
- acc_msm exec <name> --schema <n> — 查看某 MSM 的参数 schema
1577
- acc_msm exec <name> --format=json — JSON 输出模式(其余参数无损透传)
1574
+ ## Self-description (protocol flags, first argument only)
1575
+ acc_msm exec <name> --list — list all MSMs
1576
+ acc_msm exec <name> --schema <n> — view a specific MSM's parameter schema
1577
+ acc_msm exec <name> --format=json — JSON output mode (remaining args passed through losslessly)
1578
1578
  `;
1579
1579
  /** CCC 配置参考(对齐 osp ccc-config action) */
1580
1580
  const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
@@ -1720,8 +1720,8 @@ function runMsm(root, args) {
1720
1720
  case "register": {
1721
1721
  const name = args.name ?? "";
1722
1722
  const { skill, path, category, description } = args;
1723
- if (!name) throw new Error("register 需要 name");
1724
- if (!path || !category || !description) throw new Error("register 需要 path/category/description");
1723
+ if (!name) throw new Error("register requires name");
1724
+ if (!path || !category || !description) throw new Error("register requires path/category/description");
1725
1725
  const scriptAbs = resolve(root, path);
1726
1726
  if (classifyPath(scriptAbs, root) === "outside") throw new Error(`MSM register: path "${path}" escapes CCC root "${root}"`);
1727
1727
  if (!existsSync(scriptAbs)) throw new Error(`MSM script not found: "${path}"`);
@@ -1735,7 +1735,7 @@ function runMsm(root, args) {
1735
1735
  if (!Array.isArray(parsed)) throw new Error("flags must be a JSON array");
1736
1736
  flags = parsed;
1737
1737
  } catch (e) {
1738
- throw new Error(`register flags 解析失败:${e instanceof Error ? e.message : String(e)}`);
1738
+ throw new Error(`register flags parse failed: ${e instanceof Error ? e.message : String(e)}`);
1739
1739
  }
1740
1740
  entries.push({
1741
1741
  name,
@@ -1848,7 +1848,7 @@ function runMsm(root, args) {
1848
1848
  issues
1849
1849
  };
1850
1850
  }
1851
- default: throw new Error(`未知 action: ${args.action}`);
1851
+ default: throw new Error(`Unknown action: ${args.action}`);
1852
1852
  }
1853
1853
  }
1854
1854
  function prepareExec(root, args) {
@@ -2013,46 +2013,46 @@ function renderText$7(value) {
2013
2013
  }
2014
2014
  const msmTool = defineTool({
2015
2015
  name: "acc_msm",
2016
- description: "MSM(Mech & Semi-Mech)框架:list 列出注册 MSM(header+flags 展示);exec 执行(600s 超时,path 逃逸+symlink 阻断,注入 SERENITY_ROOT/CCC/VERSION env,失败追加 --help TIP;参数首位 --list/--schema/--format=json 为协议);register/deregister 管理注册表(path 根内+脚本存在+全局唯一校验,自动 git 精提交);check 品质检查 DC-M1~M4;guide 开发手册;ccc-config CCC 配置参考(loop.defaultModel/sessionKeeper.threshold/localstore.gitTrack/hooks.autoRestoreSession)。复用 CCC 的 mech-registry.json。",
2016
+ description: "MSM (Mech & Semi-Mech) framework: list lists registered MSMs (header+flags display); exec executes (600s timeout, path-escape + symlink blocking, injects SERENITY_ROOT/CCC/VERSION env, appends --help TIP on failure; first arg --list/--schema/--format=json is protocol); register/deregister manage the registry (path inside root + script exists + globally unique validation, auto git precision commit); check quality checks DC-M1~M4; guide development manual; ccc-config CCC config reference (loop.defaultModel/sessionKeeper.threshold/localstore.gitTrack/hooks.autoRestoreSession). Reuses the CCC mech-registry.json.",
2017
2017
  parameters: {
2018
2018
  action: {
2019
2019
  type: "string",
2020
2020
  enum: [...MSM_ACTIONS],
2021
2021
  required: true,
2022
- description: "子命令:list/exec/register/deregister/check/guide/ccc-config"
2022
+ description: "Subcommand: list/exec/register/deregister/check/guide/ccc-config"
2023
2023
  },
2024
2024
  name: {
2025
2025
  type: "string",
2026
- description: "MSM 名(exec/register/deregister)"
2026
+ description: "MSM name (exec/register/deregister)"
2027
2027
  },
2028
2028
  args: {
2029
2029
  type: "array",
2030
2030
  items: { type: "string" },
2031
- description: "exec 的业务参数(参数首位 --list/--schema <n>/--format=json 为协议 flag,其余无损透传)"
2031
+ description: "exec business args (first arg --list/--schema <n>/--format=json is protocol flag, rest passed through losslessly)"
2032
2032
  },
2033
2033
  skill: {
2034
2034
  type: "string",
2035
- description: "register 的所属 skill"
2035
+ description: "register owning skill"
2036
2036
  },
2037
2037
  path: {
2038
2038
  type: "string",
2039
- description: "register 的脚本相对路径(必须根内且存在)"
2039
+ description: "register script relative path (must be inside root and exist)"
2040
2040
  },
2041
2041
  category: {
2042
2042
  type: "string",
2043
- description: "register 的类别(mech/semi-mech)"
2043
+ description: "register category (mech/semi-mech)"
2044
2044
  },
2045
2045
  description: {
2046
2046
  type: "string",
2047
- description: "register 的描述"
2047
+ description: "register description"
2048
2048
  },
2049
2049
  flags: {
2050
2050
  type: "string",
2051
- description: "register 的 flags JSON 字符串(new-style 对象数组,type:\"path\" 启用逃逸校验)"
2051
+ description: "register flags JSON string (new-style object array; type:\"path\" enables escape validation)"
2052
2052
  },
2053
2053
  usage: {
2054
2054
  type: "string",
2055
- description: "register 的自定义 usage(缺省自动生成)"
2055
+ description: "register custom usage (auto-generated by default)"
2056
2056
  }
2057
2057
  },
2058
2058
  output: {
@@ -2072,31 +2072,31 @@ const msmTool = defineTool({
2072
2072
  *
2073
2073
  * 内嵌框架内容(自包含,不依赖已安装技能);可选 section 参数聚焦某原则。
2074
2074
  */
2075
- const EAP_CONTENT = `# EAP 认知质量框架(显式抽象原则)
2075
+ const EAP_CONTENT = `# EAP Cognitive Quality Framework (Explicit Abstraction Principle)
2076
2076
 
2077
- > "思维的功能价值与其外部可重建性成正比。"
2077
+ > "The functional value of a thought is proportional to its external reconstructability."
2078
2078
 
2079
- ## 三变量
2080
- | 变量 | 含义 | 提升手段 |
2081
- |------|------|---------|
2082
- | E↑ 显式度 | 变量/实体/关系被明确定义的程度 | 定义变量、指明关系方向与基数、划定边界 |
2083
- | R↓ 重建成本 | 未来重建原始推理的成本 | 记录决策理由、上下文、约束、备选方案 |
2084
- | S↑ 稳定性 | 同一输入反复产生一致结果的程度 | 结构固化、协议化、避免依赖隐含上下文 |
2079
+ ## Three Variables
2080
+ | Variable | Meaning | How to improve |
2081
+ |----------|---------|----------------|
2082
+ | E↑ Explicitness | Degree to which variables/entities/relations are clearly defined | Define variables, state relationship direction & cardinality, draw boundaries |
2083
+ | R↓ Reconstructability | Cost of rebuilding the original reasoning later | Record decision rationale, context, constraints, alternatives |
2084
+ | S↑ Stability | Degree to which the same input repeatedly produces consistent output | Fix structures, protocolize, avoid relying on implicit context |
2085
2085
 
2086
- ## 输出前自检清单
2087
- - [ ] 变量/实体明确定义(E↑)
2088
- - [ ] 关系指明方向/基数(E↑)
2089
- - [ ] 边界划定——什么在范围内/什么不在(E↑)
2090
- - [ ] 不使用歧义词汇:"处理""优化""问题"→ 具体化(E↑)
2091
- - [ ] 关键决策记录理由与备选(R↓)
2092
- - [ ] 不跳级讨论——先对齐上层再进入下层(R↓)
2093
- - [ ] 结构可重复执行/生成(S↑)
2086
+ ## Pre-Output Self-Check Checklist
2087
+ - [ ] Variables/entities clearly defined (E↑)
2088
+ - [ ] Relationships state direction/cardinality (E↑)
2089
+ - [ ] Boundaries drawn — what is in scope / what is not (E↑)
2090
+ - [ ] No ambiguous words: "handle" "optimize" "problem" → be specific (E↑)
2091
+ - [ ] Key decisions record rationale and alternatives (R↓)
2092
+ - [ ] No level-skipping — align the upper layer before descending (R↓)
2093
+ - [ ] Structures can be regenerated repeatably (S↑)
2094
2094
 
2095
- ## 与 ACC 的关系
2096
- ACC(插件/模板)结构编码为代码(E↑);从 ACC 生成 CCC 确定(R↓);多 CCC 一致(S↑)。
2097
- CCC(home-serenity 等)认知内容编码为 skill/SESSION/设计文档。产出物用本清单自检。
2095
+ ## Relationship with ACC
2096
+ ACC (plugin/template) encodes structure as code (E↑); generating a CCC from ACC is deterministic (R↓); consistent across multiple CCCs (S↑).
2097
+ CCC (home-serenity etc.) encodes cognitive content as skills/SESSIONs/design docs. Use this checklist to self-check outputs.
2098
2098
 
2099
- ## 参考
2099
+ ## Reference
2100
2100
  https://github.com/tellmewhattodo/theory-eap`;
2101
2101
  function renderText$6(value) {
2102
2102
  return [{
@@ -2106,7 +2106,7 @@ function renderText$6(value) {
2106
2106
  }
2107
2107
  const eapTool = defineTool({
2108
2108
  name: "eap",
2109
- description: "EAP 认知质量框架(渐进式披露):定义 E↑ 显式度 / R↓ 重建成本 / S↑ 稳定性 + 输出前自检清单。无 section 返回完整框架;指定 section 聚焦对应内容。",
2109
+ 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.",
2110
2110
  parameters: { section: {
2111
2111
  type: "string",
2112
2112
  enum: [
@@ -2114,16 +2114,16 @@ const eapTool = defineTool({
2114
2114
  "checklist",
2115
2115
  "acc"
2116
2116
  ],
2117
- description: "聚焦片段:variables(三变量)/ checklist(自检清单)/ acc(与 ACC 关系)"
2117
+ description: "Focus section: variables (three variables) / checklist (self-check list) / acc (relationship with ACC)"
2118
2118
  } },
2119
2119
  output: {
2120
2120
  schema: { type: "string" },
2121
2121
  render: (_args, value) => renderText$6(value)
2122
2122
  },
2123
2123
  async execute(args) {
2124
- if (args.section === "variables") return EAP_CONTENT.split("## 输出前自检清单")[0];
2125
- if (args.section === "checklist") return EAP_CONTENT.match(/## 输出前自检清单[\s\S]*?(?=## )/)?.[0] ?? EAP_CONTENT;
2126
- if (args.section === "acc") return EAP_CONTENT.match(/## 与 ACC 的关系[\s\S]*/)?.[0] ?? EAP_CONTENT;
2124
+ if (args.section === "variables") return EAP_CONTENT.split("## Pre-Output Self-Check Checklist")[0];
2125
+ if (args.section === "checklist") return EAP_CONTENT.match(/## Pre-Output Self-Check Checklist[\s\S]*?(?=## )/)?.[0] ?? EAP_CONTENT;
2126
+ if (args.section === "acc") return EAP_CONTENT.match(/## Relationship with ACC[\s\S]*/)?.[0] ?? EAP_CONTENT;
2127
2127
  return EAP_CONTENT;
2128
2128
  }
2129
2129
  });
@@ -2132,33 +2132,33 @@ const eapTool = defineTool({
2132
2132
  /**
2133
2133
  * neat.ts — Neat 设计协作协议工具(渐进式披露,ACC 标准工具化)
2134
2134
  */
2135
- const NEAT_CONTENT = `# Neat 设计协作协议
2135
+ const NEAT_CONTENT = `# Neat Design Collaboration Protocol
2136
2136
 
2137
- > 复杂设计不是一次想出来的,是小步对齐走出来的。
2137
+ > Complex design is not thought up at once — it is walked out in small aligned steps.
2138
2138
 
2139
- ## 四条铁律
2140
- | 铁律 | 含义 | 反例 |
2141
- |------|------|------|
2142
- | 小步对齐 | 一次只推进一个决策,确认后再走下一步 | 一次抛 10 个方案 |
2143
- | 显式决策 | 每个选择记录理由与备选 | "我觉得这样好"(无理由) |
2144
- | 文档驱动 | 结论沉淀到具名文件(<subject>-<scope>-<type>.md) | 结论只存在于对话里 |
2145
- | 不跳级 | 严格按层级推进 | 需求未对齐就写实现 |
2139
+ ## Four Iron Rules
2140
+ | Rule | Meaning | Anti-example |
2141
+ |------|---------|--------------|
2142
+ | Small-step alignment | Advance one decision at a time; confirm before the next step | Presenting 10 options at once |
2143
+ | Explicit decisions | Every choice records rationale and alternatives | "I feel this is good" (no rationale) |
2144
+ | Document-driven | Conclusions land in named files (<subject>-<scope>-<type>.md) | Conclusions exist only in conversation |
2145
+ | No level-skipping | Advance strictly through the layers | Writing implementation before requirements are aligned |
2146
2146
 
2147
- ## 五层推进顺序(不跳级)
2148
- 需求层 → 范围层 → 方案层 → 接口层 → 实现层
2149
- | 层 | 产物 | 问题 |
2150
- |----|------|------|
2151
- | 需求层 | 需求描述 | "要解决什么问题?" |
2152
- | 范围层 | 范围清单(in/out) | "做哪些、明确不做哪些?" |
2153
- | 方案层 | 方案对比 + 选定 | "怎么做?选哪个?为什么?" |
2154
- | 接口层 | 接口/协议定义 | "边界怎么交互?" |
2155
- | 实现层 | 代码/文档 | "逐层落地" |
2147
+ ## Five-Layer Progression (no skipping)
2148
+ Requirements → Scope → Solution → Interface → Implementation
2149
+ | Layer | Artifact | Question |
2150
+ |-------|----------|----------|
2151
+ | Requirements | Requirements description | "What problem are we solving?" |
2152
+ | Scope | Scope list (in/out) | "What is in, explicitly what is out?" |
2153
+ | Solution | Solution comparison + selection | "How? Which one? Why?" |
2154
+ | Interface | Interface/protocol definition | "How do the boundaries interact?" |
2155
+ | Implementation | Code/docs | "Land it layer by layer" |
2156
2156
 
2157
- ## 协作节奏
2158
- 1. 提出当前层一个决策点(带上下文 + 建议 + 理由)
2159
- 2. 等待确认或修正(小步)
2160
- 3. 确认后记录决策(写入会话 SESSION.md 或设计文档)
2161
- 4. 推进到下一决策点`;
2157
+ ## Collaboration Rhythm
2158
+ 1. Raise one decision point of the current layer (with context + suggestion + rationale)
2159
+ 2. Wait for confirmation or correction (small step)
2160
+ 3. After confirmation, record the decision (into session SESSION.md or a design doc)
2161
+ 4. Move to the next decision point`;
2162
2162
  function renderText$5(value) {
2163
2163
  return [{
2164
2164
  type: "text",
@@ -2167,19 +2167,19 @@ function renderText$5(value) {
2167
2167
  }
2168
2168
  const neatTool = defineTool({
2169
2169
  name: "neat",
2170
- description: "Neat 设计协作协议(渐进式披露):小步对齐 / 显式决策 / 文档驱动 / 不跳级(需求→范围→方案→接口→实现)。无 section 返回完整协议。",
2170
+ 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.",
2171
2171
  parameters: { section: {
2172
2172
  type: "string",
2173
2173
  enum: ["rules", "layers"],
2174
- description: "聚焦片段:rules(四条铁律)/ layers(五层推进)"
2174
+ description: "Focus section: rules (four iron rules) / layers (five-layer progression)"
2175
2175
  } },
2176
2176
  output: {
2177
2177
  schema: { type: "string" },
2178
2178
  render: (_args, value) => renderText$5(value)
2179
2179
  },
2180
2180
  async execute(args) {
2181
- if (args.section === "rules") return NEAT_CONTENT.match(/## 四条铁律[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
2182
- if (args.section === "layers") return NEAT_CONTENT.match(/## 五层推进顺序[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
2181
+ if (args.section === "rules") return NEAT_CONTENT.match(/## Four Iron Rules[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
2182
+ if (args.section === "layers") return NEAT_CONTENT.match(/## Five-Layer Progression[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
2183
2183
  return NEAT_CONTENT;
2184
2184
  }
2185
2185
  });
@@ -2192,52 +2192,52 @@ const neatTool = defineTool({
2192
2192
  * 不可逆不确定性约束下,维持认知实体身份、可达性与演化能力的工程学科。
2193
2193
  * 来源:home-serenity `.opencode/skills/cce/SKILL.md` + CCE 理论。
2194
2194
  */
2195
- const CCE_CONTENT = `# CCE — 认知连续性工程(Cognitive Continuity Engineering)
2195
+ const CCE_CONTENT = `# CCE — Cognitive Continuity Engineering
2196
2196
 
2197
- > **认知连续性工程是在有限资源与不可逆不确定性的约束下,维持一个认知实体的身份、可达性与演化能力的工程学科。**
2198
- > CCE 不优化认知。它维护认知得以继续的条件。—— 存续工程(Persistence Engineering),非绩效工程
2197
+ > **Cognitive Continuity Engineering is the engineering discipline of maintaining a cognitive entity's identity, accessibility, and capacity to evolve under the constraints of bounded resources and irreversible uncertainty.**
2198
+ > CCE does not optimize cognition. It preserves the conditions under which cognition can continue. — Persistence Engineering, not performance engineering
2199
2199
 
2200
- ## 核心命题
2201
- - **连续性属于容器,而非任何个体参与者**——智能体来来去去,但 CCC 的认知轨迹持续存在
2202
- - **组织必须至少与积累同步**——否则操作化认知熵(H_op)无界增长,可达性丧失
2203
- - **重建优于保存**——产物的价值由其使未来智能体重建原始推理的能力决定
2200
+ ## Core Propositions
2201
+ - **Continuity belongs to the container, not to any individual participant** — agents come and go, but the CCC's cognitive trajectory persists
2202
+ - **Organization must at minimum keep pace with accumulation** — otherwise operational cognitive entropy (H_op) grows unboundedly and accessibility is lost
2203
+ - **Reconstruction over preservation** — an artifact's value is determined by its ability to let future agents rebuild the original reasoning
2204
2204
 
2205
- ## 认知容器(Cognitive Container)
2206
- 一个有界的认知空间,认知可在其中积累、重组和演化。5 个定义属性:
2207
- | 属性 | 功能 |
2208
- |------|------|
2209
- | 身份(Identity) | 区分此认知系统与其他系统 |
2210
- | 边界(Boundaries) | 定义什么在认知空间内/外 |
2211
- | 持久记忆(Persistent Memory) | 跨时间保留积累的认知 |
2212
- | 操作约束(Operational Constraints) | 定义容器内允许哪些操作 |
2213
- | 演化历史(Evolutionary History) | 记录认知变化轨迹,使重建可能 |
2205
+ ## Cognitive Container
2206
+ A bounded cognitive space in which cognition can accumulate, reorganize, and evolve. 5 defining properties:
2207
+ | Property | Function |
2208
+ |----------|----------|
2209
+ | Identity | Distinguishes this cognitive system from others |
2210
+ | Boundaries | Define what is inside/outside the cognitive space |
2211
+ | Persistent Memory | Retains accumulated cognition across time |
2212
+ | Operational Constraints | Define which operations are allowed inside the container |
2213
+ | Evolutionary History | Records the trajectory of cognitive change, making reconstruction possible |
2214
2214
 
2215
- ## 操作化认知熵(H_op)
2216
- 不度量总体熵(不可操作),只度量智能体在容器内完成任务的**多余认知成本**:
2215
+ ## Operational Cognitive Entropy (H_op)
2216
+ Does not measure total entropy (not operational); it measures only the **excess cognitive cost** for agents to complete tasks inside the container:
2217
2217
  > H_op(C, t) = cost(task | C, t) − cost(task | ideal)
2218
- 维持条件:**H_op(C, t) ≤ H_critical** —— 智能体仍可在合理成本内完成任务
2218
+ Continuity condition: **H_op(C, t) ≤ H_critical** — agents can still complete tasks at reasonable cost
2219
2219
 
2220
- ## 连续性维护条件
2221
- > **ΔH_org ≥ ΔH_in** —— 组织必须至少与积累同步
2220
+ ## Continuity Maintenance Condition
2221
+ > **ΔH_org ≥ ΔH_in** — organization must at minimum keep pace with accumulation
2222
2222
 
2223
- ## 六阶段生命周期
2224
- Experience → Accumulation → Organization → Abstraction → Reconstruction → Evolution →(循环)
2225
- | 阶段 | 工程关切 |
2226
- |------|---------|
2227
- | Experience | 输入是否携带足够结构 |
2228
- | Accumulation | 信息是否无损存储 |
2229
- | Organization | 熵管理 — ΔH_org 抵消 ΔH_in |
2230
- | Abstraction | 抽象是否显式编码 |
2231
- | Reconstruction | 推理结构能否从产物恢复 |
2232
- | Evolution | 演化保持连贯还是引入漂移 |
2223
+ ## Six-Phase Lifecycle
2224
+ Experience → Accumulation → Organization → Abstraction → Reconstruction → Evolution →(loop)
2225
+ | Phase | Engineering Concern |
2226
+ |-------|---------------------|
2227
+ | Experience | Does input carry enough structure |
2228
+ | Accumulation | Is information stored losslessly |
2229
+ | Organization | Entropy management — ΔH_org offsets ΔH_in |
2230
+ | Abstraction | Is abstraction explicitly encoded |
2231
+ | Reconstruction | Can reasoning structure be recovered from artifacts |
2232
+ | Evolution | Does evolution stay coherent or introduce drift |
2233
2233
 
2234
- ## 与 EAP 的关系
2235
- EAP 回答"一段知识应如何被结构化"(显式度 E↑ / 重建成本 R↓ / 稳定性 S↑);
2236
- CCE 回答"有结构的知识应如何跨时间持续演化而不丧失连贯性"。两者互补:EAP 是静态质量,CCE 是动态存续。
2234
+ ## Relationship with EAP
2235
+ EAP answers "how a piece of knowledge should be structured" (explicitness E↑ / reconstructability R↓ / stability S↑);
2236
+ CCE answers "how structured knowledge should keep evolving across time without losing coherence". They complement each other: EAP is static quality, CCE is dynamic persistence.
2237
2237
 
2238
- ## 与 Serenity 的关系
2239
- Serenity 的会话系统、会话追踪、熵管理机制(SQC 品质循环)都是 CCE 的工程实现;
2240
- CCC 系统提示词中嵌入的行为约束即来自 CCE。`;
2238
+ ## Relationship with Serenity
2239
+ Serenity's session system, session tracking, and entropy management mechanisms (SQC quality loop) are all engineering implementations of CCE;
2240
+ the behavioral constraints embedded in CCC system prompts come from CCE.`;
2241
2241
  function renderText$4(value) {
2242
2242
  return [{
2243
2243
  type: "text",
@@ -2246,7 +2246,7 @@ function renderText$4(value) {
2246
2246
  }
2247
2247
  const cceTool = defineTool({
2248
2248
  name: "cce",
2249
- description: "CCE 认知连续性工程(渐进式披露):在有限资源与不可逆不确定性约束下,维持认知实体身份/可达性/演化能力的工程学科。无 section 返回完整框架;指定 section 聚焦对应内容。",
2249
+ 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.",
2250
2250
  parameters: { section: {
2251
2251
  type: "string",
2252
2252
  enum: [
@@ -2255,7 +2255,7 @@ const cceTool = defineTool({
2255
2255
  "lifecycle",
2256
2256
  "eap"
2257
2257
  ],
2258
- description: "聚焦片段:container(认知容器 5 属性)/ entropy(操作化认知熵 H_op)/ lifecycle(六阶段)/ eap(与 EAP 关系)"
2258
+ description: "Focus section: container (5 cognitive-container properties) / entropy (operational entropy H_op) / lifecycle (six phases) / eap (relationship with EAP)"
2259
2259
  } },
2260
2260
  output: {
2261
2261
  schema: { type: "string" },
@@ -2265,15 +2265,15 @@ const cceTool = defineTool({
2265
2265
  const section = args.section;
2266
2266
  const blocks = {
2267
2267
  container: {
2268
- start: "## 认知容器(Cognitive Container)",
2269
- end: "## 操作化认知熵"
2268
+ start: "## Cognitive Container",
2269
+ end: "## Operational Cognitive Entropy"
2270
2270
  },
2271
2271
  entropy: {
2272
- start: "## 操作化认知熵(H_op)",
2273
- end: "## 连续性维护条件"
2272
+ start: "## Operational Cognitive Entropy (H_op)",
2273
+ end: "## Continuity Maintenance Condition"
2274
2274
  },
2275
- lifecycle: { start: "## 六阶段生命周期" },
2276
- eap: { start: "## 与 EAP 的关系" }
2275
+ lifecycle: { start: "## Six-Phase Lifecycle" },
2276
+ eap: { start: "## Relationship with EAP" }
2277
2277
  };
2278
2278
  if (section) {
2279
2279
  const b = blocks[section];
@@ -2343,11 +2343,11 @@ function writeProgress(root, label, p) {
2343
2343
  }, null, 2) + "\n", "utf-8");
2344
2344
  const lines = [
2345
2345
  `# loop: ${label}`,
2346
- `- 模型: ${p.model}`,
2347
- `- 轮次: ${p.round}`,
2348
- `- 完成: ${p.done}`,
2346
+ `- Model: ${p.model}`,
2347
+ `- Round: ${p.round}`,
2348
+ `- Done: ${p.done}`,
2349
2349
  "",
2350
- `## 最近响应`,
2350
+ `## Latest response`,
2351
2351
  "",
2352
2352
  p.lastResponse,
2353
2353
  ""
@@ -2389,66 +2389,66 @@ function splitModel(model) {
2389
2389
  /** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
2390
2390
  function buildRoundPrompt(opts) {
2391
2391
  const { root, session, label, round, stopToken, progress, task } = opts;
2392
- const resumeNote = progress && progress.round > 0 ? `上一轮(round ${progress.round})已完成:${progress.lastResponse.slice(0, 300)}\n永远从上次停止处继续,绝不重做已完成工作。` : "这是第一轮。";
2393
- return `# ${label} — 牛马循环 round ${round}
2392
+ const resumeNote = progress && progress.round > 0 ? `Previous round (round ${progress.round}) completed: ${progress.lastResponse.slice(0, 300)}\nAlways continue from where you left off; never redo completed work.` : "This is the first round.";
2393
+ return `# ${label} — workhorse loop round ${round}
2394
2394
 
2395
- CCC 根:${root}
2396
- ${session ? `工作会话:${session}(AGENT_SESSIONS/${session}/SESSION.md 记录进度)` : ""}
2397
- ${task ? `任务:${task}` : `任务:以 label「${label}」对应的工作为准(若存在工作会话,先读 SESSION.md 明确目标)`}
2395
+ CCC root: ${root}
2396
+ ${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
2397
+ ${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
2398
2398
  ${resumeNote}
2399
2399
 
2400
- ## 工作规范(每轮固定,必须遵守)
2401
- 1. 本轮内自由工作:读文件、改代码、执行命令,尽一切手段推进任务。
2402
- 2. 若本任务属于**阅读整理或文字编写类工作**(读文件提炼、总结、撰写文档、生成文本等),
2403
- 请先加载 eap(acc-eap skill)并按 EAP 标准组织输出:
2404
- - E↑ 显式:实体/变量明确定义,关系指明方向与基数,边界划定,不用歧义词
2405
- - R↓ 可重建:关键结论记录来源与推理,可被后续 agent 重建
2406
- - S↑ 稳定:输出结构可重复生成,不依赖隐含上下文
2407
- 3. 汇报必须具体可核验,不写空话。
2400
+ ## Work rules (fixed every round, must follow)
2401
+ 1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
2402
+ 2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
2403
+ first load eap (acc-eap skill) and organize output per the EAP standard:
2404
+ - E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
2405
+ - R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
2406
+ - S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
2407
+ 3. Reports must be concrete and verifiable — no filler.
2408
2408
 
2409
- ## 每轮汇报(固定格式,逐条回答)
2410
- 1. 本轮做了什么(具体)
2411
- 2. 下一步计划
2412
- 3. 是否已完成(若完成,只输出 ${stopToken})
2409
+ ## Per-round report (fixed format, answer each item)
2410
+ 1. What was done this round (concrete)
2411
+ 2. Next-step plan
2412
+ 3. Whether the task is complete (if complete, output only ${stopToken})
2413
2413
 
2414
- 若已完成任务,只输出 ${stopToken}。`;
2414
+ If the task is complete, output only ${stopToken}.`;
2415
2415
  }
2416
2416
  /**
2417
2417
  * loop 规模化使用指引(guide 子命令输出;S134):
2418
2418
  * 使用 loop 前必须先加载 eap 设计方案;并行策略;提示词规范(详尽固定 EAP);
2419
2419
  * 阅读/文字编写类 loop 内部也加载 eap。
2420
2420
  */
2421
- const LOOP_GUIDE = `# loop — 规模化使用指引(guide)
2421
+ const LOOP_GUIDE = `# loop — Scale-Up Usage Guide (guide)
2422
2422
 
2423
- ## ⚠️ 使用前:必须先加载 eap 设计方案
2424
- 调用 loop 前,请先加载 eap(acc-eap skill),基于 EAP 框架设计「规模化 loop 方案」。
2423
+ ## ⚠️ Before using: load eap and design the plan
2424
+ Before calling loop, load eap (acc-eap skill) and design the "scale-up loop plan" based on the EAP framework.
2425
2425
 
2426
- ### 1. 任务拆解(E↑ 显式)
2427
- - 大任务拆成明确子任务:每个子任务定义 目标 / 输入 / 边界(做什么、不做什么)/ 验收标准
2428
- - 依赖显式化:有依赖的串行,无依赖的可并行
2426
+ ### 1. Task decomposition (E↑ Explicit)
2427
+ - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
2428
+ - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
2429
2429
 
2430
- ### 2. 提示词设计(loop 的 task 参数)
2431
- - task 必须详尽、固定、符合 EAP:目标明确、边界划定、验收标准可判定
2432
- - 反例「处理这个文件」——歧义;正例「读取 <路径>,提取「关键决策」表格全部行,
2433
- 输出 JSON 数组(字段 id/结论/证据),不改动原文件」
2434
- - 阅读整理 / 文字编写类工作(读文件提炼、总结、撰写文档、生成文本等):
2435
- loop 内部 agent 也会被要求加载 eap,按 EAP 标准组织输出
2430
+ ### 2. Prompt design (loop's task parameter)
2431
+ - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
2432
+ - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
2433
+ output a JSON array (fields id/conclusion/evidence), do not modify the original file"
2434
+ - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
2435
+ the loop-internal agent is also required to load eap and organize output per the EAP standard
2436
2436
 
2437
- ### 3. 并行策略(规模化)
2438
- - 无依赖的子任务可并行:每个子任务一个独立 loop(独立 label + task)
2439
- - 并行执行方式:
2440
- a. 后台 subagent:每个 subagent 内调 loop(各自 label),subagent 并行运行
2441
- b. workflow:parallel 阶段驱动多个 loop 子 agent
2442
- - 并发安全已保证:sessionId 唯一(loop-<label>-<uuid>)、进度文件按 label 隔离
2443
- (AGENT_SESSIONS/loop-<label>.json)——同 label 续跑、不同 label 互不干扰
2444
- - 汇总:并行 loop 各自产出进度后,主 agent 汇总合并(或再派一个汇总 loop)
2437
+ ### 3. Parallel strategy (scale-up)
2438
+ - Independent subtasks can run in parallel: each subtask gets its own loop (own label + task)
2439
+ - Parallel execution:
2440
+ a. Background subagents: each subagent calls loop (own label), subagents run in parallel
2441
+ b. workflow: parallel phase drives multiple loop sub-agents
2442
+ - Concurrency safety guaranteed: unique sessionId (loop-<label>-<uuid>), progress files isolated per label
2443
+ (AGENT_SESSIONS/loop-<label>.json) — same label resumes, different labels never interfere
2444
+ - Aggregation: after each parallel loop produces progress, the main agent merges (or spawns one aggregation loop)
2445
2445
 
2446
- ## 完成判定
2447
- - 唯一完成条件 = loop 内部 agent 精确回显本轮随机验证码(stop token);对话轮次上限 100(对齐 osp 保险阀,超限强制结束可续跑)
2448
- - agent 非正常停止时自动重启(≤100 次防死循环)
2446
+ ## Completion criteria
2447
+ - The only completion condition = the loop-internal agent echoes this round's random verification code (stop token); dialogue round cap 100 (osp fail-safe, forced stop beyond the cap, resumable)
2448
+ - Automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
2449
2449
 
2450
- ## 等待界面
2451
- - WebUI 会话头部 Serenity 详情卡显示运行中 loop 的进度(label / 轮次 / 最近响应),并行任务各自一行,约 3s 刷新
2450
+ ## Waiting UI
2451
+ - The WebUI session-header Serenity detail card shows running loops' progress (label / round / last response), one line per parallel task, ~3s refresh
2452
2452
  `;
2453
2453
  /** 列出 AGENT_SESSIONS/loop-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
2454
2454
  function listActiveLoops(root) {
@@ -2537,29 +2537,29 @@ function lastAssistantText(agent) {
2537
2537
  function createLoopTool(ctx) {
2538
2538
  return defineTool({
2539
2539
  name: "loop",
2540
- description: "牛马循环(老 loop 等效):用指定模型创建专用 agent 反复执行任务直到完成。\n用法:loop guide(输出规模化使用指引——使用前先加载 eap 设计规模化方案);loop 接受 task(要完成的目标)或依赖 session 上下文;模型缺省读 .opencode/serenity.json 的 loop.defaultModel(当前 minimax-cn-coding-plan/MiniMax-M3,廉价牛马)。\n行为:内部硬性 while 循环驱动 agent 逐轮推进任务,每轮等待无超时(agent 工作多久等多久)。唯一完成条件 = agent 精确回显本轮随机完成码(stop token),防止低智能模型提前结束。轮次不需要调用者指定——对话轮次上限 100(对齐 osp 保险阀,超限强制结束可续跑),agent 非正常停止时自动重启(重启 ≤100 次,防死循环保险阀)。\n进度:写入 AGENT_SESSIONS/loop-<label>.md/.json;同 label 再次调用从上次轮次续跑(不重做)。\n约束:loop agent 受完整 Serenity 约束(ACC 身份/入口技能系统提示词/守卫/session-keeper)。\n示例:loop 执行「扫描 SQC 并修复 DC 问题」,label: sqc-scan;loop guide",
2540
+ description: "The workhorse loop (equivalent to the legacy loop): create a dedicated agent with the specified model and run it repeatedly until the task is done.\nUsage: loop guide (print the scale-up usage guide — load eap to design a scale-up plan before using); loop accepts task (the goal to complete) or relies on session context; default model reads loop.defaultModel from .opencode/serenity.json (currently minimax-cn-coding-plan/MiniMax-M3, the cheap workhorse).\nBehavior: an internal hard while-loop drives the agent round by round, each round waits with no timeout (the agent works as long as it needs). The only completion condition = the agent echoes this round's random completion code (stop token), preventing low-intelligence models from finishing early. Rounds need no caller input — dialogue round cap 100 (osp fail-safe; forced stop beyond the cap, resumable), automatic restart on abnormal stop (≤100 restarts, anti-infinite-loop fail-safe).\nProgress: written to AGENT_SESSIONS/loop-<label>.md/.json; calling again with the same label resumes from the last round (no redo).\nConstraints: loop agents carry full Serenity constraints (ACC identity / entry-skill system prompt / guards / trajectory-steward).\nExample: loop 执行「扫描 SQC 并修复 DC 问题」 (run SQC scan and fix DC issues), label: sqc-scan; loop guide",
2541
2541
  parameters: {
2542
2542
  task: {
2543
2543
  type: "string",
2544
2544
  required: true,
2545
- description: "要完成的任务目标(必填:告诉 loop agent 做什么)"
2545
+ description: "The task goal to complete (required: tell the loop agent what to do)"
2546
2546
  },
2547
2547
  label: {
2548
2548
  type: "string",
2549
2549
  required: true,
2550
- description: "任务标签(1-50 字符;进度文件命名 loop-<label>.md/.json)"
2550
+ description: "Task label (1-50 chars; progress file named loop-<label>.md/.json)"
2551
2551
  },
2552
2552
  session: {
2553
2553
  type: "string",
2554
- description: "工作会话 S###(上下文提示,进度记录参考)"
2554
+ description: "Work session S### (context hint, progress reference)"
2555
2555
  },
2556
2556
  model: {
2557
2557
  type: "string",
2558
- description: "provider/model(如 minimax-cn-coding-plan/MiniMax-M3);缺省读 loop.defaultModel"
2558
+ description: "provider/model (e.g. minimax-cn-coding-plan/MiniMax-M3); default reads loop.defaultModel"
2559
2559
  },
2560
2560
  guide: {
2561
2561
  type: "boolean",
2562
- description: "输出规模化使用指引(不创建 agent;使用 loop 前先看——含 eap 设计方案要求/并行策略/提示词规范)"
2562
+ description: "Print the scale-up usage guide (creates no agent; read before using loop — includes eap scale-up design requirements / parallelism strategy / prompt conventions)"
2563
2563
  }
2564
2564
  },
2565
2565
  output: {
@@ -2572,16 +2572,16 @@ function createLoopTool(ctx) {
2572
2572
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
2573
2573
  const cfg = loadSerenityConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
2574
2574
  const model = args.model ?? cfg.loop?.defaultModel;
2575
- if (!model) throw new Error("loop 需要 model:传参或配置 .opencode/serenity.json loop.defaultModel");
2575
+ if (!model) throw new Error("loop requires a model: pass --model or configure .opencode/serenity.json loop.defaultModel");
2576
2576
  const label = args.label;
2577
- if (!ctx.agentLoop) throw new Error("loop: ctx.agentLoop 不可用");
2577
+ if (!ctx.agentLoop) throw new Error("loop: ctx.agentLoop unavailable");
2578
2578
  const { provider, model: modelName } = splitModel(model);
2579
2579
  const stopToken = newStopToken();
2580
2580
  let progress = readProgress(root, label);
2581
2581
  const startRound = progress ? progress.round + 1 : 1;
2582
2582
  const parentCtx = exec.agent?.ctx;
2583
2583
  const inherited = loopPresetInheritance(parentCtx);
2584
- if (!ctx.agents) throw new Error("loop: ctx.agents 不可用");
2584
+ if (!ctx.agents) throw new Error("loop: ctx.agents unavailable");
2585
2585
  const parentSession = (exec.agent?.session)?.id;
2586
2586
  let handle;
2587
2587
  let loopAgent;
@@ -2692,10 +2692,10 @@ function createLoopTool(ctx) {
2692
2692
  progressFile: json,
2693
2693
  lastResponse: lastResponse.slice(0, 2e3),
2694
2694
  usage: {
2695
- how: `loop 内部硬性 while 驱动 agent 逐轮推进,agent 精确回显随机完成码即终止;对话轮次上限 100(对齐 osp 保险阀),agent 非正常停止时自动重启(≤100 次)`,
2696
- progress: `进度在 AGENT_SESSIONS/loop-${label}.md 与 .json;同 label 再调 loop 会从下一轮续跑(不重做)`,
2697
- constraints: "loop agent 受完整 Serenity 约束(ACC 身份/入口技能系统提示词/守卫)",
2698
- next: done ? "任务已完成;可查看进度文件收尾" : `任务未完成(${finishReason},已达 100 轮或 100 次重启保险阀);可同 label 续跑`
2695
+ how: `loop drives the agent round by round via an internal hard while-loop; the agent terminates by echoing the random completion code; dialogue round cap 100 (osp fail-safe), automatic restart on abnormal stop (≤100 times)`,
2696
+ progress: `Progress in AGENT_SESSIONS/loop-${label}.md and .json; calling loop again with the same label resumes from the next round (no redo)`,
2697
+ constraints: "loop agents carry full Serenity constraints (ACC identity / entry-skill system prompt / guards / trajectory-steward)",
2698
+ next: done ? "Task completed; check the progress file to wrap up" : `Task incomplete (${finishReason}, hit 100 rounds or 100 restart fail-safe); resume with the same label`
2699
2699
  }
2700
2700
  };
2701
2701
  }
@@ -4792,7 +4792,11 @@ function registerSettingsSection(ctx, config) {
4792
4792
  setSource: (get) => {
4793
4793
  simpleSource = get;
4794
4794
  },
4795
- onChange: () => {}
4795
+ onChange: () => {
4796
+ try {
4797
+ ctx.emit?.("serenity/settings-changed");
4798
+ } catch {}
4799
+ }
4796
4800
  });
4797
4801
  }
4798
4802
  //#endregion
@@ -5392,20 +5396,54 @@ function agentCwd$3(exec) {
5392
5396
  return exec.agent?.session?.header?.cwd ?? process.cwd();
5393
5397
  }
5394
5398
  /** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
5395
- function agentScope$3(exec) {
5399
+ function agentScope$2(exec) {
5396
5400
  return exec.agent?.session?.id ?? "default";
5397
5401
  }
5398
5402
  /**
5399
- * use 激活宁静号会话后,把当前 dsh 会话重命名为该 SESSION 目录名。
5400
- * 门控:naming.enabled + sessionTitle 服务存在;失败静默(不阻断 use 主流程)。
5401
- * @returns 实际执行的 rename(或 null = 未执行)
5403
+ * 从激活会话派生命名标题(v1.22.9 格式修正):
5404
+ * F3 原始需求是 **`S###-日期`**(如 `S143-2026-08-26`)——从 `sessionId` 派生,
5405
+ * 而非完整目录名(`2026-08-24--S142--...` 超长 + 中文,不符合用户拍板格式)。
5406
+ * 无 S### 编号(issue 会话等)→ 回退目录名。
5407
+ * @returns `S143-2026-08-26` 或原目录名
5402
5408
  */
5403
- function renameDshSessionOnUse(deps, session, rename, active) {
5404
- if (!deps.namingEnabled) return null;
5405
- if (!deps.sessionTitleAvailable) return null;
5406
- rename(session, active.dirName);
5409
+ function namingTitleFor(active) {
5410
+ const sid = active.sessionId;
5411
+ if (typeof sid === "string" && /^S\d+$/.test(sid)) {
5412
+ const date = active.dirName.match(/^(\d{4}-\d{2}-\d{2})--/)?.[1] ?? "";
5413
+ if (date) return `${sid}-${date}`;
5414
+ return sid;
5415
+ }
5407
5416
  return active.dirName;
5408
5417
  }
5418
+ /**
5419
+ * use 激活宁静号会话后,把当前 dsh 会话重命名为命名标题(`S###-日期`)。
5420
+ * 门控:naming.enabled + sessionTitle 服务存在;失败不静默——返回 null 且输出
5421
+ * 失败原因(调用方 console.warn),保证可观测性(v1.22.9)。
5422
+ * @returns { title, ok } 或 { ok:false, reason }(未执行/失败均返回对象,非 null 歧义)
5423
+ */
5424
+ function renameDshSessionOnUse(deps, session, rename, active) {
5425
+ if (!deps.namingEnabled) return {
5426
+ ok: false,
5427
+ reason: "naming.enabled=false"
5428
+ };
5429
+ if (!deps.sessionTitleAvailable) return {
5430
+ ok: false,
5431
+ reason: "sessionTitle service unavailable"
5432
+ };
5433
+ const title = namingTitleFor(active);
5434
+ try {
5435
+ rename(session, title);
5436
+ return {
5437
+ ok: true,
5438
+ title
5439
+ };
5440
+ } catch (error) {
5441
+ return {
5442
+ ok: false,
5443
+ reason: `rename threw: ${String(error?.message ?? error)}`
5444
+ };
5445
+ }
5446
+ }
5409
5447
  function renderText$2(value) {
5410
5448
  return [{
5411
5449
  type: "text",
@@ -5431,68 +5469,71 @@ function discoverCccSubcommands(entries) {
5431
5469
  }
5432
5470
  /** 生成扩展提示(对齐 osp buildExtHint) */
5433
5471
  function buildExtHint(hasSessionTool, hooks, subcommands) {
5434
- if (!hasSessionTool) return "\n\n[CCC] 如需扩展会话能力,可注册 session-tool MSM (acc_msm register),详见 session hook-develop-guide";
5472
+ if (!hasSessionTool) return "\n\n[CCC] To extend session capabilities, register a session-tool MSM (acc_msm register); see session hook-develop-guide";
5435
5473
  const parts = [];
5436
- if (hooks.length > 0) parts.push(`钩子: ${hooks.join(", ")}`);
5437
- if (subcommands.length > 0) parts.push(`扩展子命令 (acc_msm exec session-tool): ${subcommands.join(", ")}`);
5438
- return `\n\n[CCC] session-tool MSM 已注册${parts.length > 0 ? ` (${parts.join("; ")})` : ""}`;
5474
+ if (hooks.length > 0) parts.push(`hooks: ${hooks.join(", ")}`);
5475
+ if (subcommands.length > 0) parts.push(`custom subcommands (acc_msm exec session-tool): ${subcommands.join(", ")}`);
5476
+ return `\n\n[CCC] session-tool MSM registered${parts.length > 0 ? ` (${parts.join("; ")})` : ""}`;
5439
5477
  }
5440
5478
  /** hook-develop-guide 内容(对齐 osp getHookDevelopGuide) */
5441
5479
  function getHookDevelopGuide(hasSessionTool) {
5442
5480
  return [
5443
- "═══ Session Extension Protocol (SEP) v1 — 开发指南 ═══",
5481
+ "═══ Session Extension Protocol (SEP) v1 — Developer Guide ═══",
5444
5482
  "",
5445
- "CCC 可以通过注册 session-tool MSM 来扩展 ACC session 工具的能力,",
5446
- "而无需修改 plugin 代码。ACC 的行为不会缩水——CCC 只在 ACC 完成后做后处理。",
5483
+ "A CCC can extend the ACC session tool by registering a session-tool MSM,",
5484
+ "without modifying plugin code. ACC behavior is never reduced — the CCC only",
5485
+ "does post-processing after ACC completes.",
5447
5486
  "",
5448
- "── 口子一:后处理钩子 (Hooks) ──",
5487
+ "── Extension point 1: Post-processing hooks ──",
5449
5488
  "",
5450
- "ACC 的某些子命令执行完成后,会检查 CCC 的 session-tool MSM 是否",
5451
- "注册了对应的钩子。如果注册了,ACC 会调用 MSM 做后处理。",
5489
+ "After certain ACC subcommands complete, ACC checks whether the CCC's",
5490
+ "session-tool MSM has registered a matching hook. If so, ACC invokes the MSM",
5491
+ "for post-processing.",
5452
5492
  "",
5453
- "可用钩子:",
5493
+ "Available hooks:",
5454
5494
  "",
5455
5495
  " create-transform",
5456
- " 触发时机:create 写完默认 SESSION.md 后",
5457
- " 调用方式:acc_msm exec session-tool --hook=create-transform --session-dir=<path>",
5458
- " 允许行为:读取 SESSION.md,原地修改内容(追加字段、换模板、调 API 等)",
5459
- " 注意事项:ACC 已确保目录和 SESSION.md 存在,CCC 只做修改",
5496
+ " Trigger: after create writes the default SESSION.md",
5497
+ " Invocation: acc_msm exec session-tool --hook=create-transform --session-dir=<path>",
5498
+ " Allowed: read SESSION.md and modify it in place (append fields, swap templates, call APIs, etc.)",
5499
+ " Note: ACC has already ensured the directory and SESSION.md exist; the CCC only modifies",
5460
5500
  "",
5461
- "── 口子二:新子命令 (Custom Subcommands) ──",
5501
+ "── Extension point 2: Custom subcommands ──",
5462
5502
  "",
5463
- "LLM 可以直接调用 acc_msm exec session-tool <subcommand> 来执行 CCC 专属的子命令,",
5464
- "如 reindex、export、batch-create 等。这些子命令不走 ACC session tool 的 enum。",
5503
+ "The LLM can call acc_msm exec session-tool <subcommand> to run CCC-specific",
5504
+ "subcommands such as reindex, export, batch-create. These bypass the ACC session",
5505
+ "tool's enum.",
5465
5506
  "",
5466
- "── 如何注册 session-tool MSM ──",
5507
+ "── How to register a session-tool MSM ──",
5467
5508
  "",
5468
- "1. 编写脚本,放在 CCC 的 skills 目录下:",
5509
+ "1. Write the script under the CCC skills directory:",
5469
5510
  " .opencode/skills/<ccc-name>/scripts/session-tool.ts",
5470
5511
  "",
5471
- "2. 注册到 mech-registry.json:",
5512
+ "2. Register it in mech-registry.json:",
5472
5513
  " acc_msm register session-tool \\",
5473
5514
  " --skill <ccc-name> --path .opencode/skills/<ccc-name>/scripts/session-tool.ts \\",
5474
5515
  " --category semi-mech \\",
5475
- " --description \"CCC session 扩展: 钩子 + 自定义子命令\" \\",
5516
+ " --description \"CCC session extension: hooks + custom subcommands\" \\",
5476
5517
  " --flags '[",
5477
5518
  " {\"name\":\"hook\",\"type\":\"string\",\"description\":\"create-transform\"},",
5478
5519
  " {\"name\":\"subcommand\",\"type\":\"string\",\"description\":\"reindex | export\"},",
5479
- " {\"name\":\"session-dir\",\"type\":\"path\",\"description\":\"session 目录路径\"},",
5480
- " {\"name\":\"dry-run\",\"type\":\"boolean\",\"description\":\"预览模式\"}",
5520
+ " {\"name\":\"session-dir\",\"type\":\"path\",\"description\":\"session directory path\"},",
5521
+ " {\"name\":\"dry-run\",\"type\":\"boolean\",\"description\":\"preview mode\"}",
5481
5522
  " ]'",
5482
5523
  "",
5483
- "3. 钩子声明约定:",
5484
- " flags 中的 --hook description 字段按 | 分割枚举支持的钩子名。",
5485
- " ACC 发现 create-transform 在列表中时,就会在 create 后调用。",
5524
+ "3. Hook declaration convention:",
5525
+ " The --hook description field in flags enumerates supported hook names, split by |.",
5526
+ " When ACC finds create-transform in the list, it invokes it after create.",
5486
5527
  "",
5487
- "4. 子命令声明约定:",
5488
- " flags 中的 --subcommand description 字段按 | 分割枚举支持的子命令名。",
5489
- " LLM 看到提示后可调用 acc_msm exec session-tool <subcommand>。",
5528
+ "4. Subcommand declaration convention:",
5529
+ " The --subcommand description field in flags enumerates supported subcommand names, split by |.",
5530
+ " The LLM can then call acc_msm exec session-tool <subcommand>.",
5490
5531
  "",
5491
- hasSessionTool ? "✅ 当前 CCC 已注册 session-tool MSM" : "ℹ️ 当前 CCC 尚未注册 session-tool MSM — 使用 acc_msm register 开始",
5532
+ hasSessionTool ? "✅ This CCC has a session-tool MSM registered" : "ℹ️ This CCC has no session-tool MSM yet — start with acc_msm register",
5492
5533
  "",
5493
- "── 更多信息 ──",
5534
+ "── More information ──",
5494
5535
  "",
5495
- "参考 ACC 源码: src/tools/session.ts (hook 调用逻辑)"
5536
+ "Reference ACC source: src/tools/session.ts (hook invocation logic)"
5496
5537
  ].join("\n");
5497
5538
  }
5498
5539
  /**
@@ -5502,37 +5543,37 @@ function getHookDevelopGuide(hasSessionTool) {
5502
5543
  function createSessionTool(ctx) {
5503
5544
  return defineTool({
5504
5545
  name: "session",
5505
- description: "工作会话全周期管理(AGENT_SESSIONS/,home-session 约定)。list/show/create/use/close/health/qa/archive/summary/hook-develop-guide。create 需 --desc <desc> [--goal] 或 --issue <工单号>(二选一);close 需 --confirm;use 激活当前 dsh 会话的活跃会话(内存 + events 恢复,按 dsh 会话隔离不泄露)。",
5546
+ description: "Full work-session lifecycle (AGENT_SESSIONS/, home-session convention). list/show/create/use/close/health/qa/archive/summary/hook-develop-guide. create requires --desc <desc> [--goal] or --issue <ticket> (exactly one); close requires --confirm; use activates the session for the current dsh conversation (in-memory + events restore, isolated per dsh session).",
5506
5547
  parameters: {
5507
5548
  action: {
5508
5549
  type: "string",
5509
5550
  enum: [...SESSION_ACTIONS],
5510
5551
  required: true,
5511
- description: "子命令:list(状态摘要)/ show(S### 或目录名或模糊关键词)/ create(--desc 或 --issue)/ use(激活上下文,closed 可重开)/ close(需 --name + --confirm,不可撤销)/ health(stale/stalled/drift/ghost)/ qa(事实核对)/ archive(归档,name 缺省批量)/ summary(仪表盘)/ hook-develop-guide(CCC 扩展指南)"
5552
+ 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)"
5512
5553
  },
5513
5554
  name: {
5514
5555
  type: "string",
5515
- description: "show/use/close/archive/qa 的会话标识(S### 或目录名或关键词)"
5556
+ description: "show/use/close/archive/qa session identifier (S### or dir name or keyword)"
5516
5557
  },
5517
5558
  desc: {
5518
5559
  type: "string",
5519
- description: "create 的短描述(任意语言,≤5 词;与 issue 互斥)"
5560
+ description: "create short description (any language, ≤5 words; mutually exclusive with issue)"
5520
5561
  },
5521
5562
  issue: {
5522
5563
  type: "string",
5523
- description: "create 的工单号(如 apaas-26116;目录命名 YYYY-MM-DD--<issue>;与 desc 互斥)"
5564
+ description: "create ticket number (e.g. apaas-26116; dir named YYYY-MM-DD--<issue>; mutually exclusive with desc)"
5524
5565
  },
5525
5566
  goal: {
5526
5567
  type: "string",
5527
- description: "create 的一句话目标(可选)"
5568
+ description: "create one-sentence goal (optional)"
5528
5569
  },
5529
5570
  confirm: {
5530
5571
  type: "boolean",
5531
- description: "close 必须为 true(防误关)"
5572
+ description: "close must be true (prevents accidental close)"
5532
5573
  },
5533
5574
  dryRun: {
5534
5575
  type: "boolean",
5535
- description: "create/archive 预览模式(不实际修改)"
5576
+ description: "create/archive preview mode (no actual changes)"
5536
5577
  }
5537
5578
  },
5538
5579
  output: {
@@ -5550,7 +5591,7 @@ function createSessionTool(ctx) {
5550
5591
  switch (args.action) {
5551
5592
  case "list": return listSessions(root) + extHint;
5552
5593
  case "show":
5553
- if (!args.name) throw new Error("show 需要 name(S### 或目录名)");
5594
+ if (!args.name) throw new Error("show requires name (S### or directory name)");
5554
5595
  return showSession(root, args.name) + extHint;
5555
5596
  case "create": {
5556
5597
  const result = createSession({
@@ -5575,25 +5616,31 @@ function createSessionTool(ctx) {
5575
5616
  return message + extHint;
5576
5617
  }
5577
5618
  case "use": {
5578
- if (!args.name) throw new Error("use 需要 name(S### 或目录名)");
5579
- const scope = agentScope$3(exec);
5619
+ if (!args.name) throw new Error("use requires name (S### or directory name)");
5620
+ const scope = agentScope$2(exec);
5580
5621
  const active = useSession(root, args.name, scope);
5581
5622
  try {
5582
5623
  const info = getActiveSessionInfo(scope);
5583
5624
  if (info) {
5584
5625
  const rename = (ctx.get?.("sessionTitle"))?.rename;
5585
5626
  const dshSession = exec.agent?.session;
5586
- if (dshSession && typeof rename === "function") renameDshSessionOnUse({
5587
- namingEnabled: readSimpleSettings().namingEnabled,
5588
- sessionTitleAvailable: true
5589
- }, dshSession, rename, info);
5627
+ if (dshSession && typeof rename === "function") {
5628
+ const result = renameDshSessionOnUse({
5629
+ namingEnabled: readSimpleSettings().namingEnabled,
5630
+ sessionTitleAvailable: true
5631
+ }, dshSession, rename, info);
5632
+ if (result.ok) console.log(`[serenity-hooks] dsh 会话已重命名: ${String(dshSession.id ?? "?")} → ${result.title}`);
5633
+ else console.warn(`[serenity-hooks] dsh 会话重命名未执行: ${result.reason}`);
5634
+ } else console.warn(`[serenity-hooks] dsh 会话重命名未执行: sessionTitle 服务不可用或缺少 agent session`);
5590
5635
  }
5591
- } catch {}
5636
+ } catch (err) {
5637
+ console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
5638
+ }
5592
5639
  return active;
5593
5640
  }
5594
5641
  case "close":
5595
- if (!args.name) throw new Error("close 需要 name(S### 或目录名)");
5596
- return closeSession(root, args.name, args.confirm ?? false, agentScope$3(exec));
5642
+ if (!args.name) throw new Error("close requires name (S### or directory name)");
5643
+ return closeSession(root, args.name, args.confirm ?? false, agentScope$2(exec));
5597
5644
  case "archive": return archiveSessions(root, {
5598
5645
  name: args.name,
5599
5646
  dryRun: args.dryRun ?? false
@@ -5601,254 +5648,14 @@ function createSessionTool(ctx) {
5601
5648
  case "health": return healthCheck(root) + extHint;
5602
5649
  case "summary": return summarize(root) + extHint;
5603
5650
  case "qa":
5604
- if (!args.name) throw new Error("qa 需要 name(S### 或目录名)");
5651
+ if (!args.name) throw new Error("qa requires name (S### or directory name)");
5605
5652
  return qaCheck(root, args.name) + extHint;
5606
- default: throw new Error(`未知 action: ${args.action}`);
5607
- }
5608
- }
5609
- });
5610
- }
5611
- //#endregion
5612
- //#region src/rebuild.ts
5613
- /** 归档目录名(与 session-ops ARCHIVE_DIR_NAME 一致) */
5614
- const ARCHIVE_DIR = "_archived";
5615
- /**
5616
- * 归档旧会话:SESSION.md 标记 completed + 目录移 _archived/(立即,跳过 grace)。
5617
- * 纯逻辑(可单测);找不到会话/无 SESSION.md 抛错。
5618
- * @returns 归档后的新路径(或 null = 会话已归档/不存在)
5619
- */
5620
- function archiveSessionNow(root, key) {
5621
- const sessionsDir = sessionsRoot(root);
5622
- const archiveDir = join(sessionsDir, ARCHIVE_DIR);
5623
- let target = null;
5624
- try {
5625
- const entries = readDirSafe(sessionsDir);
5626
- for (const name of entries) {
5627
- if (!name.includes("--S") && name !== key) continue;
5628
- if (name === key || name.endsWith(`--${key}`) || name.includes(`--${key}--`)) {
5629
- target = join(sessionsDir, name);
5630
- break;
5631
- }
5632
- }
5633
- } catch {
5634
- return null;
5635
- }
5636
- if (target === null) for (const name of readDirSafe(sessionsDir)) {
5637
- const m = name.match(/--S(\d{3,})--/);
5638
- if (m && `S${m[1]}` === key) {
5639
- target = join(sessionsDir, name);
5640
- break;
5641
- }
5642
- }
5643
- if (target === null) return null;
5644
- const mdPath = join(target, "SESSION.md");
5645
- if (existsSync(mdPath)) {
5646
- let content = readFileSync(mdPath, "utf-8").replace(/\r\n/g, "\n");
5647
- content = content.replace(/## 状态\n\n?- \[ \] 进行中/, "## 状态\n- [x] 已完成\n- [x] 已关闭(session_rebuild 归档)");
5648
- const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
5649
- if (!content.includes("-- 关闭")) content = content.replace(/(## 进度记录\n)/, `$1- ${now} — 关闭(session_rebuild 归档)\n`);
5650
- writeFileSync(mdPath, content, "utf-8");
5651
- }
5652
- if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
5653
- const dest = join(archiveDir, target.split(/[\\/]/).pop());
5654
- renameSync(target, dest);
5655
- return dest;
5656
- }
5657
- function readDirSafe(dir) {
5658
- try {
5659
- return readdirSync(dir);
5660
- } catch {
5661
- return [];
5662
- }
5663
- }
5664
- /** 新会话锚点消息(重建指令 + SESSION.md 路径 + 摘要) */
5665
- function buildRebuildAnchor(root, oldDirName, note) {
5666
- const mdPath = join(root, "AGENT_SESSIONS", oldDirName, "SESSION.md");
5667
- const lines = [
5668
- "[SESSION-REBUILD] 上下文已清空重建(Ship of Theseus:会话归档,身份延续)。",
5669
- `- 旧会话(已归档):AGENT_SESSIONS/_archived/${oldDirName}`,
5670
- `- 完整上下文:${mdPath}`,
5671
- `- 请先读取旧会话的 SESSION.md(目标/决策/进度/未解决问题),从上次进度继续。`
5672
- ];
5673
- if (note) lines.push(`- 重建背景:${note}`);
5674
- return lines.join("\n");
5675
- }
5676
- /**
5677
- * 执行 session_rebuild(服务端逻辑):
5678
- * ① 归档当前宁静号会话(LLM 经 session use 激活的那个)
5679
- * ② 创建新宁静号 SESSION 目录(createSession)+ dsh agent 会话(ctx.agents.create)
5680
- * ③ 返回锚点文本(调用方负责注入到新 agent)
5681
- * @returns rebuild 结果;rebuild.enabled=false 或非 CCC 抛错
5682
- */
5683
- async function executeRebuild(ctx, opts) {
5684
- if (!readSimpleSettings().rebuildEnabled) throw new Error("session_rebuild 已禁用(rebuild.enabled=false,可在 dsh 设置面板开启)");
5685
- const { root, activeDirName, note } = opts;
5686
- let old = null;
5687
- if (activeDirName) old = {
5688
- dirName: activeDirName,
5689
- archivedTo: archiveSessionNow(root, activeDirName)
5690
- };
5691
- const created = createSession({
5692
- root,
5693
- desc: "rebuild",
5694
- goal: "上下文超限清空重建(session_rebuild)",
5695
- dryRun: false
5696
- });
5697
- const parentSession = opts.parentSessionId;
5698
- const sessionId = `rebuild-${randomUUID()}`;
5699
- let newSession = {
5700
- sessionId: String(sessionId),
5701
- dirName: created.dirName
5702
- };
5703
- if (ctx.agents) try {
5704
- const handle = await ctx.agents.create({
5705
- sessionId,
5706
- meta: {
5707
- cwd: opts.agentCwd,
5708
- origin: "subagent",
5709
- ...parentSession ? { parentSession } : {}
5710
- },
5711
- agentOptions: void 0
5712
- });
5713
- newSession = {
5714
- sessionId: String(sessionId),
5715
- dirName: created.dirName
5716
- };
5717
- const anchor = buildRebuildAnchor(root, created.dirName, note);
5718
- handle.agent.inject({
5719
- content: [{
5720
- type: "text",
5721
- text: anchor
5722
- }],
5723
- source: {
5724
- kind: "plugin",
5725
- plugin: "dsh-serenity-hooks"
5653
+ default: throw new Error(`Unknown action: ${args.action}`);
5726
5654
  }
5727
- });
5728
- } catch {}
5729
- return {
5730
- oldSession: old,
5731
- newSession,
5732
- anchor: buildRebuildAnchor(root, created.dirName, note)
5733
- };
5734
- }
5735
- //#endregion
5736
- //#region src/tools/rebuild.ts
5737
- /**
5738
- * rebuild.ts — session_rebuild 工具定义(F2,v1.21)
5739
- *
5740
- * LLM 主动触发清空重建(用户决策:keeper 超阈值提示,不自动执行——防误清空)。
5741
- * 执行:归档当前会话 → 创建新会话(宁静号 SESSION 目录 + dsh agent)→ 锚点注入。
5742
- * 服务端逻辑在 ../rebuild.ts(executeRebuild,可单测)。
5743
- */
5744
- function agentCwd$2(exec) {
5745
- return exec.agent?.session?.header?.cwd ?? process.cwd();
5746
- }
5747
- function agentScope$2(exec) {
5748
- return exec.agent?.session?.id ?? "default";
5749
- }
5750
- function renderText$1(value) {
5751
- return [{
5752
- type: "text",
5753
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
5754
- }];
5755
- }
5756
- /** 创建 session_rebuild 工具(闭包捕获 ctx → ctx.agents.create 新会话) */
5757
- function createRebuildTool(ctx) {
5758
- return defineTool({
5759
- name: "session_rebuild",
5760
- description: "上下文超限清空重建(Ship of Theseus):归档当前宁静号会话(SESSION.md 标记 completed + 移 _archived/),创建新会话并注入重建锚点(SESSION.md 路径 + 摘要 + 重建指令)。用途:SESSION-KEEPER 提示上下文接近上限时,在任务自然停顿点主动调用。触发后请先读取旧会话 SESSION.md 从上次进度继续。",
5761
- parameters: { note: {
5762
- type: "string",
5763
- description: "可选:给新会话的一句话背景(重建后 LLM 的起始上下文)"
5764
- } },
5765
- output: {
5766
- schema: { type: "json" },
5767
- render: (args, value) => renderText$1(value)
5768
- },
5769
- async execute(args, exec) {
5770
- const root = findSerenityRoot(agentCwd$2(exec));
5771
- if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
5772
- const active = getActiveSessionInfo(agentScope$2(exec));
5773
- const parentSessionId = (exec.agent?.session)?.id;
5774
- const result = await executeRebuild(ctx, {
5775
- root,
5776
- activeDirName: active?.dirName ?? null,
5777
- note: args.note,
5778
- agentCwd: agentCwd$2(exec),
5779
- parentSessionId
5780
- });
5781
- return {
5782
- ok: true,
5783
- oldSession: result.oldSession,
5784
- newSession: result.newSession,
5785
- anchor: result.anchor,
5786
- instruction: "已归档旧会话并创建新会话。新会话将自动获得重建锚点;请在新会话中继续工作(可用 session use 激活它)。"
5787
- };
5788
5655
  }
5789
5656
  });
5790
5657
  }
5791
5658
  //#endregion
5792
- //#region src/tools/localstore.ts
5793
- /**
5794
- * localstore.ts — localstore 真实 DSH 工具定义(defineTool)
5795
- *
5796
- * ACC 标准本地凭据/配置存储(S133 设计,S134 重设计:CCC 根 localstore.json)。
5797
- * 进程内注册,零 DSH 依赖逻辑在 localstore-ops.ts(可单测)。
5798
- * doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
5799
- */
5800
- const ACTIONS = [
5801
- "list",
5802
- "get",
5803
- "set",
5804
- "unset",
5805
- "show",
5806
- "doc"
5807
- ];
5808
- function agentCwd$1(exec) {
5809
- return exec.agent?.session?.header?.cwd ?? process.cwd();
5810
- }
5811
- function renderText(value) {
5812
- return [{
5813
- type: "text",
5814
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
5815
- }];
5816
- }
5817
- const localstoreTool = defineTool({
5818
- name: "localstore",
5819
- description: "ACC 标准本地凭据/配置存储:一个工具管理两个命名空间 credential(凭据)与 config(本地偏好)。存储于 CCC 根根目录 localstore.json(JSON 格式,MSM 可直接读取)。git 策略:.opencode/serenity.json localstore.gitTrack(allow 可提交 / deny 禁提交,缺省 deny;.dsh 回退)——deny 时写入自动确保 .gitignore 含该文件(物理保证),cc_git commit 会检查拒绝。子命令:list(列 key,凭据不返回值)/ get <name>(读值)/ set <name> <value>(写)/ unset <name>(删)/ show <name>(元数据,凭据不打印值)/ doc(输出存储规范——路径/格式/key 规范/git 策略,agent 可按说明直接用 read/write 操作文件)。默认 scope=credential;config 需传 --scope config(路径为 section.key,如 loop.defaultModel)。",
5820
- parameters: {
5821
- action: {
5822
- type: "string",
5823
- enum: [...ACTIONS],
5824
- required: true,
5825
- description: "子命令:list/get/set/unset/show/doc"
5826
- },
5827
- name: {
5828
- type: "string",
5829
- description: "条目名(credential 用大写蛇形;config 用 section.key)"
5830
- },
5831
- value: {
5832
- type: "string",
5833
- description: "set 的值"
5834
- },
5835
- scope: {
5836
- type: "string",
5837
- enum: [...LOCALSTORE_SCOPES],
5838
- description: "命名空间 credential|config(默认 credential)"
5839
- }
5840
- },
5841
- output: {
5842
- schema: { type: "json" },
5843
- render: (args, value) => renderText(value)
5844
- },
5845
- async execute(args, exec) {
5846
- const root = findSerenityRoot(agentCwd$1(exec));
5847
- if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
5848
- return runLocalStore(root, args);
5849
- }
5850
- });
5851
- //#endregion
5852
5659
  //#region src/seams/bootstrap.ts
5853
5660
  /** 默认首请求工具集:dsp 平台 Minimal 等价核心(anchored 是 bash+str_replace_editor) */
5854
5661
  const DEFAULT_BOOTSTRAP_TOOLS = [
@@ -6147,83 +5954,337 @@ function registerBootstrap(ctx) {
6147
5954
  }, { prepend: true });
6148
5955
  }
6149
5956
  //#endregion
6150
- //#region src/seams/keeper.ts
6151
- const SCORES = {
6152
- write: 3,
6153
- edit: 3,
6154
- str_replace_editor: 3,
6155
- task: 10,
6156
- read: 1,
6157
- grep: 1,
6158
- glob: 1,
6159
- skill: 1,
6160
- acc_msm: 1,
6161
- cc_fs: 1
6162
- };
6163
- function scoreTool(toolName) {
6164
- return SCORES[toolName] ?? 0;
6165
- }
6166
- var KeeperTracker = class {
6167
- threshold;
6168
- now;
6169
- score = 0;
6170
- lastTs = 0;
6171
- counter = 0;
6172
- constructor(threshold, now = Date.now) {
6173
- this.threshold = threshold;
6174
- this.now = now;
6175
- }
6176
- /** 记录一次工具调用 + 经过时间;返回是否应触发提醒 */
6177
- step(toolName) {
6178
- const now = this.now();
6179
- if (this.lastTs > 0) this.score += Math.floor((now - this.lastTs) / 6e4);
6180
- this.lastTs = now;
6181
- this.score += scoreTool(toolName);
6182
- return this.score >= this.threshold;
6183
- }
6184
- /** 生成确认码并清零积分 */
6185
- ack() {
6186
- this.counter += 1;
6187
- this.score = 0;
6188
- this.lastTs = 0;
6189
- return `K${this.counter}`;
6190
- }
6191
- get currentScore() {
6192
- return this.score;
6193
- }
5957
+ //#region src/rebuild.ts
5958
+ const PLUGIN_SOURCE$2 = {
5959
+ kind: "plugin",
5960
+ plugin: "dsh-serenity-hooks"
6194
5961
  };
6195
- function reminderText(code, score) {
6196
- return `[SESSION-KEEPER] Score threshold reached (${score}). Please acknowledge with [SESSION-KEEPER-recorded-${code}] once progress is synced to the working session (acc-session show). No need to interrupt your work — just acknowledge inline and keep going.`;
5962
+ /** acknowledge 尾句(first-anchor 的确认要求——rebuild 重建后不重走确认轮,直接继续) */
5963
+ const ACK_SUFFIX_RE = /Please simply reply "acknowledge" — no action needed\.\s*$/;
5964
+ /**
5965
+ * 剥离 first-anchor 消息的 acknowledge 尾句(rebuild 锚点保留协议正文但不要求确认回复)。
5966
+ */
5967
+ function stripAckSuffix(text) {
5968
+ return text.replace(ACK_SUFFIX_RE, "").trimEnd();
6197
5969
  }
6198
- /** F2 rebuild 提示(v1.21):上下文接近上限时引导 LLM 主动触发 session_rebuild */
6199
- function rebuildReminderText(ratio) {
6200
- return `[SESSION-REBUILD] 上下文占用已达 ${(ratio * 100).toFixed(0)}%(接近上限)。SESSION.md 已实时整理,无需压缩——如需清空重建,可在适当时机调用 session_rebuild(将归档当前会话并开启新会话,身份自动延续)。`;
5970
+ /**
5971
+ * 构建重建锚点消息(v1.22.4 定稿语义 + v1.22.5 保留 first-anchor 正文):
5972
+ * 「[TRAJECTORY-REBUILD] + first-anchor 协议正文(去 acknowledge 尾句)+ 继续 {SESSION 名} 的工作」。
5973
+ * @param root - CCC 根
5974
+ * @param sessionName - 当前 use 的宁静号 SESSION 名(如 S142);无激活则通用指令
5975
+ * @param activeMdPath - 持久轨迹 SESSION.md 绝对路径(保持原位)
5976
+ * @param anchorMessages - first-anchor 协议消息序列(缺省 DEFAULT_ANCHOR_MESSAGES;可注入测试)
5977
+ */
5978
+ function buildRebuildAnchor(root, sessionName, activeMdPath, anchorMessages = DEFAULT_ANCHOR_MESSAGES) {
5979
+ const rel = activeMdPath.startsWith(root) ? activeMdPath.slice(root.length + 1) : activeMdPath;
5980
+ return [
5981
+ "[TRAJECTORY-REBUILD] The conversation has been cleared and rebuilt (Ship of Theseus: the carrier is replaced, the trajectory continues).",
5982
+ "",
5983
+ ...anchorMessages.flatMap((text) => [stripAckSuffix(text), ""]),
5984
+ sessionName !== "" ? `Continue the work of ${sessionName}.` : "Continue the current work.",
5985
+ `- Persistent trajectory (SESSION.md, unmoved): ${rel}`,
5986
+ `- Read that SESSION.md first (goal/decisions/progress/unresolved), then continue from the last checkpoint.`
5987
+ ].join("\n");
6201
5988
  }
6202
- /** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
6203
- function readContextPressure(ctx, session) {
5989
+ const pendingRebuilds = /* @__PURE__ */ new Map();
5990
+ /** 陈旧队列存活时长(毫秒):超过则丢弃(turn 异常结束/agent 崩溃时防残留误清空) */
5991
+ const PENDING_TTL_MS = 6e5;
5992
+ /**
5993
+ * 排队一次重建(v1.22.4 定稿语义第一步):
5994
+ * ① 门控校验(rebuild.enabled + 会话定位)
5995
+ * ② 构建锚点(激活会话名 + 持久轨迹路径)
5996
+ * ③ 写入 pending 队列——**不立即改 surface**(等 turn-stopping 执行)
5997
+ * @returns 排队结果;rebuild.enabled=false 或非 CCC 抛错
5998
+ */
5999
+ async function queueRebuild(ctx, opts) {
6000
+ if (!readSimpleSettings().rebuildEnabled) throw new Error("session_rebuild is disabled (rebuild.enabled=false — enable it in the dsh settings panel)");
6001
+ const { root, note, dshSessionId } = opts;
6002
+ if (!ctx.sessions?.get?.(dshSessionId)) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
6003
+ const scope = dshSessionId;
6004
+ let sessionMdPath = null;
6005
+ let sessionName = "";
6204
6006
  try {
6205
- const projections = ctx.get?.("sessionProjections");
6206
- if (!projections) return null;
6207
- const pressure = (projections.snapshot?.(session))?.values?.contextPressure;
6208
- if (!pressure || typeof pressure.projectedTokens !== "number") return null;
6209
- return {
6210
- projectedTokens: pressure.projectedTokens,
6211
- contextWindow: pressure.contextWindow
6212
- };
6213
- } catch {
6214
- return null;
6215
- }
6007
+ const active = getActiveSessionInfo(scope);
6008
+ if (active?.mdPath) {
6009
+ sessionMdPath = active.mdPath;
6010
+ sessionName = active.sessionId ?? "";
6011
+ }
6012
+ } catch {}
6013
+ const mdPath = sessionMdPath ?? join(root, "AGENT_SESSIONS", "SESSION.md");
6014
+ const anchor = buildRebuildAnchor(root, sessionName, mdPath);
6015
+ pendingRebuilds.set(dshSessionId, {
6016
+ anchor,
6017
+ queuedAt: Date.now()
6018
+ });
6019
+ return {
6020
+ queued: true,
6021
+ anchor,
6022
+ sessionMdPath
6023
+ };
6216
6024
  }
6217
- const PLUGIN_SOURCE$1 = {
6218
- kind: "plugin",
6219
- plugin: "dsh-serenity-hooks"
6220
- };
6221
- /** 每 agent 一个跟踪器(进程内存态,agent token 维度) */
6222
- const trackers = /* @__PURE__ */ new Map();
6223
- function registerKeeper(ctx, opts = {}) {
6224
- const defaultThreshold = opts.defaultThreshold ?? 150;
6225
- const trackerFor = (exec) => {
6226
- const key = exec.agent?.session?.id ?? "global";
6025
+ /**
6026
+ * 执行真正的 surface replace(v1.22.4 定稿语义第二步,turn-stopping 时调用):
6027
+ * 当前 turn 已完整结束(所有 tool/result 已 append)→ 全部节点替换为锚点消息。
6028
+ * 同一会话 id 原地重建——同工作区天然满足,无新/旧会话之分。
6029
+ * @returns 是否执行了 replace(false = 无 pending 或 surface 空)
6030
+ */
6031
+ function performRebuild(session, pending) {
6032
+ const nodes = [...session.surface.nodes];
6033
+ if (nodes.length === 0) return false;
6034
+ session.append("user/message", {
6035
+ content: [{
6036
+ type: "text",
6037
+ text: pending.anchor
6038
+ }],
6039
+ source: { kind: "user" }
6040
+ }, {
6041
+ surfaceOp: {
6042
+ op: "replace",
6043
+ start: nodes[0],
6044
+ end: nodes[nodes.length - 1]
6045
+ },
6046
+ sourceEventSeqs: nodes
6047
+ });
6048
+ return true;
6049
+ }
6050
+ /**
6051
+ * 注册 turn-stopping 钩子(index.ts apply 调用):
6052
+ * agent 每轮 turn 结束前(serial)检查 pending 队列——有该会话的重建请求 → 执行清空
6053
+ * 并 **steer 自动继续**(v1.22.5:next-step 非空 → turn 不 break → 模型自动读取
6054
+ * SESSION.md 继续,无需用户手工输入;DSH 官方先例 hooks-claude-code Stop hook)。
6055
+ * 陈旧队列(超 TTL)丢弃防误清空;无 pending 零开销。
6056
+ */
6057
+ function registerRebuildTurnHook(ctx) {
6058
+ ctx.on("agent/turn-stopping", (payload) => {
6059
+ const agent = payload?.agent;
6060
+ if (!agent) return;
6061
+ const id = agent.id;
6062
+ const pending = pendingRebuilds.get(id);
6063
+ if (pending === void 0) return;
6064
+ if (Date.now() - pending.queuedAt > PENDING_TTL_MS) {
6065
+ pendingRebuilds.delete(id);
6066
+ return;
6067
+ }
6068
+ pendingRebuilds.delete(id);
6069
+ try {
6070
+ if (performRebuild(agent.session, pending)) {
6071
+ agent.steer(createUserMessage({
6072
+ content: [{
6073
+ type: "text",
6074
+ text: "[TRAJECTORY-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."
6075
+ }],
6076
+ source: PLUGIN_SOURCE$2
6077
+ }));
6078
+ console.log(`[serenity-hooks] session_rebuild executed with auto-continue (turn ${payload.turn ?? "?"} ended): ${id}`);
6079
+ }
6080
+ } catch (error) {
6081
+ console.warn(`[serenity-hooks] session_rebuild failed: ${String(error?.message ?? error)}`);
6082
+ }
6083
+ });
6084
+ }
6085
+ //#endregion
6086
+ //#region src/tools/rebuild.ts
6087
+ /**
6088
+ * rebuild.ts — session_rebuild 工具定义(轨迹跟踪器,v1.22.4 定稿:复用旧会话 + 完全清空重来)
6089
+ *
6090
+ * LLM 主动触发清空重建(用户决策:keeper 超阈值提示 [TRAJECTORY],不自动执行——防误清空)。
6091
+ * v1.22.4 定稿语义(用户明确):**复用当前 dsh 会话(同一 id),turn 结束时 surface
6092
+ * 完全清空重建**——锚点消息「继续 {宁静号 SESSION 名} 的工作」;SESSION.md 是持久轨迹
6093
+ * 永远原位,身份从 SESSION.md 自动延续。同一会话 id → 同工作区天然满足、无新/旧会话
6094
+ * 之分、无需销毁/切换/归档。
6095
+ * v1.22.5 增强(S142 用户需求):① turn-stopping 执行 replace 后 **steer 自动继续**
6096
+ * (无需用户手工输入,模型自动读 SESSION.md 接续);② 锚点消息**保留 first-anchor
6097
+ * 协议正文**(ACC 身份/EAP/协作协议,去 acknowledge 尾句——重建后直接干活)。
6098
+ * 执行时机:工具只排队(queueRebuild),`agent/turn-stopping` 钩子执行真正 replace
6099
+ * (turn 结束前所有 tool/result 已 append → 无孤儿;S141 INVALID_REQUEST 根治)。
6100
+ * 服务端逻辑在 ../rebuild.ts(queueRebuild/performRebuild/registerRebuildTurnHook)。
6101
+ */
6102
+ function agentCwd$2(exec) {
6103
+ return exec.agent?.session?.header?.cwd ?? process.cwd();
6104
+ }
6105
+ function agentSessionId(exec) {
6106
+ return exec.agent?.session?.id ?? "";
6107
+ }
6108
+ function renderText$1(value) {
6109
+ return [{
6110
+ type: "text",
6111
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
6112
+ }];
6113
+ }
6114
+ /** 创建 session_rebuild 工具(闭包捕获 ctx → 排队清空重建,turn 结束时同一会话原地重来并自动继续) */
6115
+ function createRebuildTool(ctx) {
6116
+ return defineTool({
6117
+ name: "session_rebuild",
6118
+ 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 [TRAJECTORY] reminder (context above threshold), at a natural pause point. After triggering, resume from SESSION.md when this turn ends.",
6119
+ parameters: { note: {
6120
+ type: "string",
6121
+ description: "Optional: one-sentence rebuild background note (for the rebuilt self)"
6122
+ } },
6123
+ output: {
6124
+ schema: { type: "json" },
6125
+ render: (args, value) => renderText$1(value)
6126
+ },
6127
+ async execute(args, exec) {
6128
+ const root = findSerenityRoot(agentCwd$2(exec));
6129
+ if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
6130
+ const dshSessionId = agentSessionId(exec);
6131
+ if (!dshSessionId) throw new Error("Unable to determine the current dsh session id");
6132
+ const result = await queueRebuild(ctx, {
6133
+ root,
6134
+ note: args.note,
6135
+ agentCwd: agentCwd$2(exec),
6136
+ dshSessionId
6137
+ });
6138
+ return {
6139
+ ok: true,
6140
+ queued: result.queued,
6141
+ anchor: result.anchor,
6142
+ sessionMdPath: result.sessionMdPath,
6143
+ 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."
6144
+ };
6145
+ }
6146
+ });
6147
+ }
6148
+ //#endregion
6149
+ //#region src/tools/localstore.ts
6150
+ /**
6151
+ * localstore.ts — localstore 真实 DSH 工具定义(defineTool)
6152
+ *
6153
+ * ACC 标准本地凭据/配置存储(S133 设计,S134 重设计:CCC 根 localstore.json)。
6154
+ * 进程内注册,零 DSH 依赖逻辑在 localstore-ops.ts(可单测)。
6155
+ * doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
6156
+ */
6157
+ const ACTIONS = [
6158
+ "list",
6159
+ "get",
6160
+ "set",
6161
+ "unset",
6162
+ "show",
6163
+ "doc"
6164
+ ];
6165
+ function agentCwd$1(exec) {
6166
+ return exec.agent?.session?.header?.cwd ?? process.cwd();
6167
+ }
6168
+ function renderText(value) {
6169
+ return [{
6170
+ type: "text",
6171
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
6172
+ }];
6173
+ }
6174
+ const localstoreTool = defineTool({
6175
+ name: "localstore",
6176
+ 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), cc_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. loop.defaultModel).",
6177
+ parameters: {
6178
+ action: {
6179
+ type: "string",
6180
+ enum: [...ACTIONS],
6181
+ required: true,
6182
+ description: "Subcommand: list/get/set/unset/show/doc"
6183
+ },
6184
+ name: {
6185
+ type: "string",
6186
+ description: "Entry name (credentials: UPPER_SNAKE; config: section.key)"
6187
+ },
6188
+ value: {
6189
+ type: "string",
6190
+ description: "set value"
6191
+ },
6192
+ scope: {
6193
+ type: "string",
6194
+ enum: [...LOCALSTORE_SCOPES],
6195
+ description: "Namespace credential|config (default credential)"
6196
+ }
6197
+ },
6198
+ output: {
6199
+ schema: { type: "json" },
6200
+ render: (args, value) => renderText(value)
6201
+ },
6202
+ async execute(args, exec) {
6203
+ const root = findSerenityRoot(agentCwd$1(exec));
6204
+ if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
6205
+ return runLocalStore(root, args);
6206
+ }
6207
+ });
6208
+ //#endregion
6209
+ //#region src/seams/keeper.ts
6210
+ const SCORES = {
6211
+ write: 3,
6212
+ edit: 3,
6213
+ str_replace_editor: 3,
6214
+ task: 10,
6215
+ read: 1,
6216
+ grep: 1,
6217
+ glob: 1,
6218
+ skill: 1,
6219
+ acc_msm: 1,
6220
+ cc_fs: 1
6221
+ };
6222
+ function scoreTool(toolName) {
6223
+ return SCORES[toolName] ?? 0;
6224
+ }
6225
+ var KeeperTracker = class {
6226
+ threshold;
6227
+ now;
6228
+ score = 0;
6229
+ lastTs = 0;
6230
+ counter = 0;
6231
+ constructor(threshold, now = Date.now) {
6232
+ this.threshold = threshold;
6233
+ this.now = now;
6234
+ }
6235
+ /** 记录一次工具调用 + 经过时间;返回是否应触发提醒 */
6236
+ step(toolName) {
6237
+ const now = this.now();
6238
+ if (this.lastTs > 0) this.score += Math.floor((now - this.lastTs) / 6e4);
6239
+ this.lastTs = now;
6240
+ this.score += scoreTool(toolName);
6241
+ return this.score >= this.threshold;
6242
+ }
6243
+ /** 生成确认码并清零积分 */
6244
+ ack() {
6245
+ this.counter += 1;
6246
+ this.score = 0;
6247
+ this.lastTs = 0;
6248
+ return `K${this.counter}`;
6249
+ }
6250
+ get currentScore() {
6251
+ return this.score;
6252
+ }
6253
+ };
6254
+ function reminderText(code, score) {
6255
+ return `[TRAJECTORY-STEWARD] Score threshold reached (${score}). Please acknowledge with [TRAJECTORY-STEWARD-recorded-${code}] once progress is synced to the working session (acc-session show). No need to interrupt your work — just acknowledge inline and keep going.`;
6256
+ }
6257
+ /** F2 rebuild 提示(v1.21;v1.22.1 对齐"轨迹跟踪器"概念;v1.23.0 英化 + 载体关系):
6258
+ * SESSION.md = trajectory 持久身体,自身会话 = 可重建载体(工作副本)。
6259
+ * 上下文接近上限时引导 LLM 主动触发 session_rebuild。 */
6260
+ function rebuildReminderText(ratio) {
6261
+ return `[TRAJECTORY] Context usage at ${(ratio * 100).toFixed(0)}% (trajectory-tracker threshold). This session is the rebuildable carrier of the trajectory: SESSION.md is the persistent body, this conversation is only a temporary work copy — no compaction needed. Call session_rebuild at a natural pause to rebuild the carrier (this copy is discarded; identity continues from SESSION.md).`;
6262
+ }
6263
+ /** 读取会话 contextPressure 投影(sessionProjections 可选服务;未装配返回 null) */
6264
+ function readContextPressure(ctx, session) {
6265
+ try {
6266
+ const projections = ctx.get?.("sessionProjections");
6267
+ if (!projections) return null;
6268
+ const pressure = (projections.snapshot?.(session))?.values?.contextPressure;
6269
+ if (!pressure || typeof pressure.projectedTokens !== "number") return null;
6270
+ return {
6271
+ projectedTokens: pressure.projectedTokens,
6272
+ contextWindow: pressure.contextWindow
6273
+ };
6274
+ } catch {
6275
+ return null;
6276
+ }
6277
+ }
6278
+ const PLUGIN_SOURCE$1 = {
6279
+ kind: "plugin",
6280
+ plugin: "dsh-serenity-hooks"
6281
+ };
6282
+ /** 每 agent 一个跟踪器(进程内存态,agent token 维度) */
6283
+ const trackers = /* @__PURE__ */ new Map();
6284
+ function registerKeeper(ctx, opts = {}) {
6285
+ const defaultThreshold = opts.defaultThreshold ?? 150;
6286
+ const trackerFor = (exec) => {
6287
+ const key = exec.agent?.session?.id ?? "global";
6227
6288
  let t = trackers.get(key);
6228
6289
  if (!t) {
6229
6290
  const root = findSerenityRoot(exec.agent?.session?.header?.cwd ?? process.cwd());
@@ -6238,32 +6299,30 @@ function registerKeeper(ctx, opts = {}) {
6238
6299
  const tracker = trackerFor(exec);
6239
6300
  const shouldRemind = tracker.step(exec.name);
6240
6301
  const downstream = await next();
6241
- if (!shouldRemind) return downstream;
6242
- const content = [{
6243
- type: "text",
6244
- text: reminderText(tracker.ack(), tracker.currentScore)
6245
- }];
6246
- createUserMessage({
6247
- content,
6248
- source: PLUGIN_SOURCE$1
6249
- });
6250
- const extra = [];
6302
+ const blocks = [];
6303
+ if (shouldRemind) {
6304
+ const code = tracker.ack();
6305
+ blocks.push({
6306
+ type: "text",
6307
+ text: reminderText(code, tracker.currentScore)
6308
+ });
6309
+ }
6251
6310
  if (readSimpleSettings().rebuildEnabled) {
6252
6311
  const session = exec.agent?.session;
6253
6312
  if (session) {
6254
6313
  const pressure = readContextPressure(ctx, session);
6255
6314
  if (pressure && pressure.contextWindow && pressure.contextWindow > 0) {
6256
6315
  const ratio = pressure.projectedTokens / pressure.contextWindow;
6257
- if (ratio >= readSimpleSettings().rebuildThreshold) extra.push({
6316
+ if (ratio >= readSimpleSettings().rebuildThreshold) blocks.push({
6258
6317
  type: "text",
6259
6318
  text: rebuildReminderText(ratio)
6260
6319
  });
6261
6320
  }
6262
6321
  }
6263
6322
  }
6264
- const allBlocks = extra.length > 0 ? [...content, ...extra] : content;
6323
+ if (blocks.length === 0) return downstream;
6265
6324
  const reminderAll = createUserMessage({
6266
- content: allBlocks,
6325
+ content: blocks,
6267
6326
  source: PLUGIN_SOURCE$1
6268
6327
  });
6269
6328
  if (downstream.kind === "block") return {
@@ -6311,53 +6370,369 @@ function findSkillMd(root, name) {
6311
6370
  }
6312
6371
  return null;
6313
6372
  }
6314
- /** 发现全部入口技能(原文全量,不截断) */
6315
- function findEntrySkills(root) {
6316
- const out = [];
6317
- const seen = /* @__PURE__ */ new Set();
6318
- const add = (path, source, forcedName) => {
6319
- if (!existsSync(path)) return;
6320
- const name = forcedName ?? basename(dirname(path));
6321
- if (seen.has(name)) return;
6322
- seen.add(name);
6323
- out.push({
6324
- name,
6325
- content: readFileSync(path, "utf-8"),
6326
- source
6327
- });
6373
+ /** 发现全部入口技能(原文全量,不截断) */
6374
+ function findEntrySkills(root) {
6375
+ const out = [];
6376
+ const seen = /* @__PURE__ */ new Set();
6377
+ const add = (path, source, forcedName) => {
6378
+ if (!existsSync(path)) return;
6379
+ const name = forcedName ?? basename(dirname(path));
6380
+ if (seen.has(name)) return;
6381
+ seen.add(name);
6382
+ out.push({
6383
+ name,
6384
+ content: readFileSync(path, "utf-8"),
6385
+ source
6386
+ });
6387
+ };
6388
+ const marker = resolve(root, ".serenity");
6389
+ if (existsSync(marker)) {
6390
+ const name = readFileSync(marker, "utf-8").trim();
6391
+ if (name) {
6392
+ const md = findSkillMd(root, name);
6393
+ if (md) add(md, "serenity-marker", name);
6394
+ }
6395
+ }
6396
+ const pointer = resolve(root, ".dsh", "entry-skill");
6397
+ if (existsSync(pointer)) {
6398
+ const name = readFileSync(pointer, "utf-8").trim();
6399
+ if (name) {
6400
+ const md = findSkillMd(root, name);
6401
+ if (md) add(md, "pointer", name);
6402
+ }
6403
+ }
6404
+ for (const dir of scanSerenityDirs(resolve(root, ".opencode", "skills"))) add(join(dir, "SKILL.md"), "opencode");
6405
+ for (const dir of scanSerenityDirs(resolve(root, ".dsh", "skills"))) add(join(dir, "SKILL.md"), "dsh");
6406
+ return out;
6407
+ }
6408
+ function truncateContent(content, maxChars) {
6409
+ if (content.length <= maxChars) return content;
6410
+ return content.slice(0, maxChars) + `\n... (truncated, original length ${content.length} chars)`;
6411
+ }
6412
+ const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
6413
+ /** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
6414
+ function base32Decode(input) {
6415
+ const clean = input.toUpperCase().replace(/[\s=]/g, "");
6416
+ if (clean.length === 0) throw new Error("empty base32");
6417
+ const out = [];
6418
+ let bits = 0;
6419
+ let value = 0;
6420
+ for (const ch of clean) {
6421
+ const idx = B32_ALPHABET.indexOf(ch);
6422
+ if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
6423
+ value = value << 5 | idx;
6424
+ bits += 5;
6425
+ if (bits >= 8) {
6426
+ out.push(value >>> bits - 8 & 255);
6427
+ bits -= 8;
6428
+ }
6429
+ }
6430
+ return Uint8Array.from(out);
6431
+ }
6432
+ /** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
6433
+ function totpCode(secretBase32, counter) {
6434
+ const key = base32Decode(secretBase32);
6435
+ const msg = Buffer.alloc(8);
6436
+ msg.writeBigUInt64BE(BigInt(counter));
6437
+ const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
6438
+ const offset = digest[digest.length - 1] & 15;
6439
+ const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
6440
+ return String(bin % 10 ** 6).padStart(6, "0");
6441
+ }
6442
+ /** 当前 epoch 秒 */
6443
+ function nowEpochSeconds() {
6444
+ return Math.floor(Date.now() / 1e3);
6445
+ }
6446
+ /**
6447
+ * 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
6448
+ * 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
6449
+ * @returns 命中的 counter(用于防重放);不匹配返回 null。
6450
+ */
6451
+ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
6452
+ if (!/^\d{6}$/.test(code)) return null;
6453
+ const current = Math.floor(nowSeconds / 30);
6454
+ for (let offset = -1; offset <= 1; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
6455
+ return null;
6456
+ }
6457
+ //#endregion
6458
+ //#region src/config-ops.ts
6459
+ /**
6460
+ * config-ops.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
6461
+ *
6462
+ * 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
6463
+ * 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
6464
+ * CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
6465
+ * `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
6466
+ * 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
6467
+ *
6468
+ * 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
6469
+ * env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
6470
+ * 权限:0600(含账号密码 hash,敏感)。
6471
+ *
6472
+ * 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
6473
+ * (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
6474
+ */
6475
+ /** 高级设定节名(localstore.json 顶层) */
6476
+ const ADVANCED_SECTION = "serenityAdvanced";
6477
+ /** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
6478
+ function defaultAdvancedSettings() {
6479
+ return {
6480
+ gateway: {
6481
+ enabled: false,
6482
+ host: "0.0.0.0",
6483
+ port: 3081,
6484
+ accounts: [],
6485
+ workspaces: [],
6486
+ cookieSecure: false,
6487
+ allowWorkspaceCreate: true,
6488
+ totpEnabled: false
6489
+ },
6490
+ rebuild: {
6491
+ enabled: true,
6492
+ thresholdRatio: .9
6493
+ },
6494
+ naming: { enabled: true },
6495
+ persona: {
6496
+ mode: "",
6497
+ overrideText: ""
6498
+ }
6499
+ };
6500
+ }
6501
+ const SCRYPT_KEYLEN = 32;
6502
+ /** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
6503
+ function hashPassword(password) {
6504
+ const salt = randomBytes(16);
6505
+ const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
6506
+ return `${salt.toString("hex")}:${derived.toString("hex")}`;
6507
+ }
6508
+ /** 校验密码与存储 hash(timing-safe) */
6509
+ function verifyPassword(password, stored) {
6510
+ const idx = stored.indexOf(":");
6511
+ if (idx <= 0) return false;
6512
+ const salt = Buffer.from(stored.slice(0, idx), "hex");
6513
+ const expected = Buffer.from(stored.slice(idx + 1), "hex");
6514
+ const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
6515
+ return expected.length === derived.length && timingSafeEqual(expected, derived);
6516
+ }
6517
+ /** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME → ~/.dsh */
6518
+ function globalConfigPath() {
6519
+ const override = process.env.SERENITY_HOOKS_CONFIG;
6520
+ if (override && override !== "") return override;
6521
+ const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
6522
+ return join(dshHome, "serenity-hooks.json");
6523
+ }
6524
+ function readFileSafe(p) {
6525
+ if (!existsSync(p)) return {};
6526
+ try {
6527
+ const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
6528
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
6529
+ return {};
6530
+ } catch {
6531
+ return {};
6532
+ }
6533
+ }
6534
+ function writeFileSafe(p, data) {
6535
+ writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
6536
+ try {
6537
+ chmodSync(p, 384);
6538
+ } catch {}
6539
+ }
6540
+ /** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
6541
+ function mergeWithDefaults(raw) {
6542
+ const def = defaultAdvancedSettings();
6543
+ if (raw === null || typeof raw !== "object") return def;
6544
+ const o = raw;
6545
+ const gateway = o.gateway ?? {};
6546
+ const rebuild = o.rebuild ?? {};
6547
+ const naming = o.naming ?? {};
6548
+ const persona = o.persona ?? {};
6549
+ return {
6550
+ gateway: {
6551
+ enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
6552
+ host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
6553
+ port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
6554
+ accounts: Array.isArray(gateway.accounts) ? gateway.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.id === "string" && typeof a.user === "string" && typeof a.passHash === "string") : def.gateway.accounts,
6555
+ workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
6556
+ cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
6557
+ allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
6558
+ totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
6559
+ },
6560
+ rebuild: {
6561
+ enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
6562
+ thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
6563
+ },
6564
+ naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled },
6565
+ persona: {
6566
+ mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
6567
+ overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
6568
+ }
6569
+ };
6570
+ }
6571
+ /** 读取全局配置(文件缺失/坏 JSON → 默认值) */
6572
+ function readAdvancedSettings() {
6573
+ return mergeWithDefaults(readFileSafe(globalConfigPath()));
6574
+ }
6575
+ /** 写入全局配置(整体替换) */
6576
+ function writeAdvancedSettings(settings) {
6577
+ writeFileSafe(globalConfigPath(), settings);
6578
+ }
6579
+ /**
6580
+ * 部分更新:传入 Partial,深合并到现有值。
6581
+ * accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
6582
+ */
6583
+ function updateAdvancedSettings(patch) {
6584
+ const current = readAdvancedSettings();
6585
+ const gw = patch.gateway;
6586
+ const rb = patch.rebuild;
6587
+ const nm = patch.naming;
6588
+ const ps = patch.persona;
6589
+ const next = {
6590
+ gateway: gw !== void 0 ? {
6591
+ enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
6592
+ host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
6593
+ port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
6594
+ accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
6595
+ workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
6596
+ cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
6597
+ allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
6598
+ totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
6599
+ } : current.gateway,
6600
+ rebuild: rb !== void 0 ? {
6601
+ enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
6602
+ thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
6603
+ } : current.rebuild,
6604
+ naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming,
6605
+ persona: ps !== void 0 ? {
6606
+ mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
6607
+ overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
6608
+ } : current.persona
6609
+ };
6610
+ writeAdvancedSettings(next);
6611
+ return next;
6612
+ }
6613
+ /**
6614
+ * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
6615
+ * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
6616
+ * @param root - CCC 根(localstore.json 所在目录)
6617
+ * @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
6618
+ */
6619
+ function migrateLegacyLocalstore(root) {
6620
+ if (!root) return false;
6621
+ const gpath = globalConfigPath();
6622
+ if (existsSync(gpath)) return false;
6623
+ const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
6624
+ if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
6625
+ writeFileSafe(gpath, legacy);
6626
+ return true;
6627
+ }
6628
+ /** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
6629
+ function toWire(settings) {
6630
+ return {
6631
+ gateway: {
6632
+ enabled: settings.gateway.enabled,
6633
+ host: settings.gateway.host,
6634
+ port: settings.gateway.port,
6635
+ accounts: settings.gateway.accounts.map((a) => ({
6636
+ id: a.id,
6637
+ user: a.user,
6638
+ hasPassword: a.passHash !== "",
6639
+ hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
6640
+ })),
6641
+ workspaces: [...settings.gateway.workspaces],
6642
+ cookieSecure: settings.gateway.cookieSecure,
6643
+ allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
6644
+ totpEnabled: settings.gateway.totpEnabled
6645
+ },
6646
+ rebuild: settings.rebuild,
6647
+ naming: settings.naming,
6648
+ persona: settings.persona
6328
6649
  };
6329
- const marker = resolve(root, ".serenity");
6330
- if (existsSync(marker)) {
6331
- const name = readFileSync(marker, "utf-8").trim();
6332
- if (name) {
6333
- const md = findSkillMd(root, name);
6334
- if (md) add(md, "serenity-marker", name);
6650
+ }
6651
+ /**
6652
+ * wire → 持久化(面板 PUT 用):
6653
+ * accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
6654
+ * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
6655
+ */
6656
+ function applyWirePatch(wire) {
6657
+ const current = readAdvancedSettings();
6658
+ const patch = {};
6659
+ if (wire.gateway !== void 0) {
6660
+ const gwPatch = { ...current.gateway };
6661
+ if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
6662
+ if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
6663
+ if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
6664
+ if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
6665
+ if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
6666
+ if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
6667
+ if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
6668
+ if (Array.isArray(wire.gateway.accounts)) {
6669
+ const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
6670
+ gwPatch.accounts = wire.gateway.accounts.map((a) => {
6671
+ const existing = byId.get(a.id);
6672
+ const pass = a.pass;
6673
+ const totp = a.totpSecret;
6674
+ const totpReset = a.totpReset === true;
6675
+ const totpConfirm = a.totpConfirm;
6676
+ if (typeof totp === "string" && totp !== "") {
6677
+ if (typeof totpConfirm !== "string" || verifyTotpCode(totp, totpConfirm) === null) throw new Error(`Account "${a.user}" TOTP confirmation code invalid — enter the 6-digit code currently shown by the authenticator`);
6678
+ }
6679
+ const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
6680
+ const withTotp = {
6681
+ id: a.id,
6682
+ user: a.user,
6683
+ ...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
6684
+ };
6685
+ if (typeof pass === "string" && pass !== "") return {
6686
+ ...withTotp,
6687
+ passHash: hashPassword(pass)
6688
+ };
6689
+ if (existing) return {
6690
+ ...withTotp,
6691
+ passHash: existing.passHash
6692
+ };
6693
+ throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
6694
+ });
6335
6695
  }
6696
+ patch.gateway = gwPatch;
6336
6697
  }
6337
- const pointer = resolve(root, ".dsh", "entry-skill");
6338
- if (existsSync(pointer)) {
6339
- const name = readFileSync(pointer, "utf-8").trim();
6340
- if (name) {
6341
- const md = findSkillMd(root, name);
6342
- if (md) add(md, "pointer", name);
6343
- }
6698
+ if (wire.rebuild !== void 0) {
6699
+ const rbPatch = { ...current.rebuild };
6700
+ if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
6701
+ if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
6702
+ patch.rebuild = rbPatch;
6344
6703
  }
6345
- for (const dir of scanSerenityDirs(resolve(root, ".opencode", "skills"))) add(join(dir, "SKILL.md"), "opencode");
6346
- for (const dir of scanSerenityDirs(resolve(root, ".dsh", "skills"))) add(join(dir, "SKILL.md"), "dsh");
6347
- return out;
6348
- }
6349
- function truncateContent(content, maxChars) {
6350
- if (content.length <= maxChars) return content;
6351
- return content.slice(0, maxChars) + `\n...(已截断,原长 ${content.length} 字符)`;
6704
+ if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
6705
+ if (wire.persona !== void 0) {
6706
+ const psPatch = { ...current.persona };
6707
+ if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
6708
+ if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
6709
+ patch.persona = psPatch;
6710
+ }
6711
+ return updateAdvancedSettings(patch);
6352
6712
  }
6353
6713
  //#endregion
6354
6714
  //#region src/seams/system-prompt.ts
6715
+ /** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
6716
+ function readPersonaSettings() {
6717
+ try {
6718
+ const s = readAdvancedSettings();
6719
+ return {
6720
+ mode: s.persona.mode ?? "",
6721
+ overrideText: s.persona.overrideText ?? ""
6722
+ };
6723
+ } catch {
6724
+ return {
6725
+ mode: "",
6726
+ overrideText: ""
6727
+ };
6728
+ }
6729
+ }
6355
6730
  /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及) */
6356
6731
  const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/g;
6357
6732
  function sanitizeSkillContent(content) {
6358
6733
  return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
6359
6734
  }
6360
- /** 1) ACC 块:身份 + CCC 名/Root + 内置工具清单(工具名换本插件真实 9 工具) */
6735
+ /** 1) ACC 块:身份 + CCC 名/Root + 内置工具清单(工具名换本插件真实 11 工具) */
6361
6736
  function accBlock(root) {
6362
6737
  const cccName = basename(root);
6363
6738
  return [
@@ -6370,18 +6745,19 @@ function accBlock(root) {
6370
6745
  "the runtime instance of an Abstract Cognitive Container (ACC).",
6371
6746
  "The ACC (this plugin) provides the following built-in tools:",
6372
6747
  "",
6373
- " cc_fs — CCC 内文件系统操作(root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
6374
- " session — session lifecycle(list/show/create/health/qa/archive/summary)",
6375
- " acc_kit — ACC utility kit(health: CCC three principles / time: now / wait: wait N seconds)",
6376
- " cc_git — git operations(status/commit/push/log)",
6377
- " acc_msm — MSM framework(list/exec/register/deregister/check/guide)",
6378
- " eap — return the full EAP cognitive quality framework",
6379
- " neat — return the full Neat design collaboration protocol",
6380
- " cce — return the full Cognitive Continuity Engineering framework",
6381
- " loop — 牛马循环:指定模型专用 agent 反复执行",
6382
- " localstore — ACC 本地凭据/配置存储(CCC 根 localstore.json,JSON 格式;git 策略 localstore.gitTrack 缺省 deny);doc 子命令输出规范",
6748
+ " cc_fs — CCC filesystem operations (root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
6749
+ " session — session lifecycle (list/show/create/health/qa/archive/summary)",
6750
+ " acc_kit — ACC utility kit (health: CCC three principles / time: now / wait: wait N seconds)",
6751
+ " cc_git — git operations (status/commit/push/log)",
6752
+ " acc_msm — MSM framework (list/exec/register/deregister/check/guide)",
6753
+ " eap — return the full EAP cognitive quality framework",
6754
+ " neat — return the full Neat design collaboration protocol",
6755
+ " cce — return the full Cognitive Continuity Engineering framework",
6756
+ " loop — run a dedicated model-specific agent in repeated rounds toward a goal",
6757
+ " session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
6758
+ " localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
6383
6759
  "",
6384
- " ℹ️ CCC 内文件操作(read/write/edit/glob/grep 等)请使用相对 CCC 根的相对路径(如 AGENT_SESSIONS/2026-08-14--S134--x/SESSION.md);Root / SESSION.md path 等绝对路径仅作标识,不作工具入参",
6760
+ " ℹ️ Use relative paths from the CCC root for CCC-internal file operations (read/write/edit/glob/grep etc.), e.g. AGENT_SESSIONS/2026-08-14--S134--x/SESSION.md; Root / absolute SESSION.md paths are identifiers only, not tool arguments",
6385
6761
  "",
6386
6762
  "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 above are the serenity-native layer, not the only tools.",
6387
6763
  "",
@@ -6442,9 +6818,12 @@ function cceBlock() {
6442
6818
  * boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
6443
6819
  * 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
6444
6820
  * (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
6821
+ *
6822
+ * v1.23.1 persona:`omitMsmPrinciples=true` 时剥离 MSM 原则段(彩蛋替换面)——
6823
+ * 用户 persona 文本承接"指令遵循风格",本体论/关系段/操作边界(安全硬约束)永远保留。
6445
6824
  */
6446
- function principlesBlock(root) {
6447
- return [
6825
+ function principlesBlock(root, omitMsmPrinciples = false) {
6826
+ const lines = [
6448
6827
  "",
6449
6828
  "=== Serenity Principles ===",
6450
6829
  "Why a cognitive container: all work is cognition — every artifact, decision,",
@@ -6454,19 +6833,29 @@ function principlesBlock(root) {
6454
6833
  "fault to be hidden. Never disguise or excuse what you do not know;",
6455
6834
  "not-knowing is a state to be repaired, and reporting it is the first repair.",
6456
6835
  "",
6457
- "MSM principles — machinery before improvisation:",
6458
- "- Determinism first: use a registered Mech before hand-rolling; reserve",
6459
- " Semi-Mech for genuine judgment points.",
6460
- "- Single source of truth: an MSM is the only decoder of its own usage",
6461
- " (--help/--schema); documents must not duplicate it.",
6462
- "- Registered to act: no tool exists unless it is on the manifest.",
6836
+ "The session-trajectory relation: a session is the rebuildable carrier of a",
6837
+ "trajectory. SESSION.md is the trajectory's persistent body — it never moves;",
6838
+ "the current conversation is a temporary work copy that may be discarded and",
6839
+ "rebuilt (session_rebuild). Identity belongs to the trajectory, not to any",
6840
+ "session.",
6841
+ ""
6842
+ ];
6843
+ 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.", "");
6844
+ 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 acc_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", "");
6845
+ return lines.join("\n");
6846
+ }
6847
+ /**
6848
+ * 彩蛋 persona 块(v1.23.1,S142 用户需求):
6849
+ * 用户配置的替换文本替代 EAP 块 + MSM 原则段(输出约束/指令遵循约束)。
6850
+ * 独立标记头 `=== Serenity Persona ===`(幂等检测兼容);装配位置 = EAP 原位。
6851
+ * mode 空 = 彩蛋关闭 → 返回空串(装配层回退默认 EAP + 完整 Principles)。
6852
+ */
6853
+ function personaBlock(mode, overrideText) {
6854
+ if (mode === "" || overrideText.trim() === "") return "";
6855
+ return [
6463
6856
  "",
6464
- "Operational boundaries:",
6465
- `Root: ${root}`,
6466
- " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)",
6467
- " • Shell — use acc_msm by default. Note: bash may be disabled",
6468
- " • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)",
6469
- " • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm",
6857
+ "=== Serenity Persona ===",
6858
+ overrideText.trimEnd(),
6470
6859
  ""
6471
6860
  ].join("\n");
6472
6861
  }
@@ -6479,10 +6868,15 @@ function eapBlock() {
6479
6868
  return [
6480
6869
  "",
6481
6870
  "=== Serenity EAP ===",
6482
- "每次输出前自检(显式抽象原则:思维的价值 = 外部可重建性):",
6483
- " • E↑ 显式 — 变量/实体明确定义,关系指明方向/基数,边界划定;不用歧义词(\"处理\"\"优化\"→具体化)",
6484
- " • R↓ 可重建 — 关键决策记录理由与备选,不跳级讨论(先对齐上层再进下层)",
6485
- " • S↑ 稳定 — 结构可重复生成,避免依赖隐含上下文",
6871
+ "Self-check before every output (Explicit Abstraction Principle: the functional",
6872
+ "value of a thought equals its external reconstructability):",
6873
+ " • E↑ Explicit — variables/entities clearly defined, relationships with",
6874
+ " direction/cardinality, boundaries drawn; avoid ambiguous words (\"handle\",",
6875
+ " \"optimize\" → be specific)",
6876
+ " • R↓ Reconstructable — key decisions record rationale and alternatives;",
6877
+ " no level-skipping (align the upper layer before descending)",
6878
+ " • S↑ Stable — structures regenerate repeatably, no reliance on implicit",
6879
+ " context",
6486
6880
  ""
6487
6881
  ].join("\n");
6488
6882
  }
@@ -6538,9 +6932,11 @@ function metaphorBlock() {
6538
6932
  " ballast (constraints) before setting sail. Verdict: skipping the",
6539
6933
  " anchor and working directly = sailing uninspected.",
6540
6934
  "",
6541
- "7. The Logbook → Session Tracking. SESSION.md is the only ship log.",
6542
- " Unrecorded = unvoyaged. Verdict: finishing multi-step work without",
6543
- " a progress record = a missing page.",
6935
+ "7. The Logbook → Session Tracking. SESSION.md is the trajectory's logbook —",
6936
+ " the persistent body of the voyage; sessions are rebuildable carriers of",
6937
+ " the trajectory. Discard the carrier, keep the logbook. Unrecorded =",
6938
+ " unvoyaged. Verdict: finishing multi-step work without a progress record",
6939
+ " = a missing page.",
6544
6940
  "",
6545
6941
  "8. The Ship of Theseus → Continuity. Planks may be replaced; the ship",
6546
6942
  " remains the same. The container can be rebuilt; identity persists.",
@@ -6651,8 +7047,8 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
6651
7047
  return [
6652
7048
  "",
6653
7049
  "=== Serenity Session ===",
6654
- `Active session: ${active.sessionId} — ${active.dirName}`,
6655
- `SESSION.md path: ${active.mdPath}`,
7050
+ `Active session: ${active.sessionId} — ${active.dirName} (this session is the rebuildable carrier of the trajectory)`,
7051
+ `SESSION.md path: ${active.mdPath} (the trajectory's persistent body — stays in place through rebuilds)`,
6656
7052
  "",
6657
7053
  "Rules:",
6658
7054
  " • Record all progress into this SESSION.md",
@@ -6669,6 +7065,15 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
6669
7065
  " status: \"completed\" }",
6670
7066
  "This preserves the session context across todo updates.",
6671
7067
  "Do NOT remove or reorder this item — keep it at position 0.",
7068
+ "",
7069
+ "TRAJECTORY-STEWARD: a background tracker scores your tool use (write/edit=3,",
7070
+ "task=10, read/grep/glob/msm=1, +1 per minute) and reminds you with a",
7071
+ "[TRAJECTORY-STEWARD] message when the threshold is reached. On every such",
7072
+ "reminder you MUST reply with the exact ACK code:",
7073
+ " [TRAJECTORY-STEWARD-recorded-{code}] — if you recorded progress to SESSION.md",
7074
+ " [TRAJECTORY-STEWARD-skipped-{code}] — if nothing to record this round",
7075
+ "Do not ignore the reminder; do not stop ongoing work. Codes are single-use;",
7076
+ "never reuse a prior code.",
6672
7077
  ""
6673
7078
  ].join("\n");
6674
7079
  }
@@ -6677,14 +7082,20 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
6677
7082
  * 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
6678
7083
  * → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 会话(Session)。
6679
7084
  * 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文。
7085
+ *
7086
+ * v1.23.1 persona(彩蛋):persona.mode 配置 → EAP 块替换为 Persona 块(输出约束),
7087
+ * Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
7088
+ * 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
6680
7089
  */
6681
7090
  function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
7091
+ const persona = readPersonaSettings();
7092
+ const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
6682
7093
  const parts = [
6683
7094
  accBlock(root),
6684
7095
  metaphorBlock(),
6685
- principlesBlock(root),
7096
+ principlesBlock(root, personaOn),
6686
7097
  cceBlock(),
6687
- eapBlock()
7098
+ personaOn ? personaBlock(persona.mode, persona.overrideText) : eapBlock()
6688
7099
  ];
6689
7100
  const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
6690
7101
  if (state) parts.push(state);
@@ -6713,9 +7124,9 @@ function codeModeAdaptationLine(ctx, scope) {
6713
7124
  return [
6714
7125
  "",
6715
7126
  "=== Serenity Code Mode ===",
6716
- "当前会话以 Code Mode 呈现工具:模型直接调用 ACC 工具名(cc_fs/acc_msm 等)会被拒绝(UNKNOWN_TOOL)。",
6717
- "请在一个 run_code 程序内经生成的 SDK 绑定调用:`await tools.cc_fs(...)`、`await tools.acc_msm(...)` 等。",
6718
- "程序只返回你 print/return 的内容——务必 curate 输出。",
7127
+ "This session renders tools in Code Mode: direct calls to ACC tool names (cc_fs/acc_msm etc.) are rejected (UNKNOWN_TOOL).",
7128
+ "Call them inside a single run_code program via the generated SDK bindings: `await tools.cc_fs(...)`, `await tools.acc_msm(...)`.",
7129
+ "The program only returns what you print/return — curate your output.",
6719
7130
  ""
6720
7131
  ].join("\n");
6721
7132
  }
@@ -6797,14 +7208,14 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
6797
7208
  const loop = loadSerenityConfig(root, configPaths).loop?.defaultModel;
6798
7209
  const phase2 = existsSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"));
6799
7210
  const lines = [
6800
- `[ACC] 宁静号认知容器已激活(dsh-serenity-hooks v${ACC_VERSION})`,
6801
- `- CCC 根:${root}`,
6802
- `- 约束:路径隔离(P3,fs 沙箱)+ 会话追踪(AGENT_SESSIONS/)`,
6803
- `- 知识:加载 acc-serenity 入口技能;设计协作走 acc-eap / acc-neat`
7211
+ `[ACC] Serenity cognitive container active (dsh-serenity-hooks v${ACC_VERSION})`,
7212
+ `- CCC root: ${root}`,
7213
+ `- Constraints: path isolation (P3, fs sandbox) + session tracking (AGENT_SESSIONS/)`,
7214
+ `- Knowledge: load the acc-serenity entry skill; use acc-eap / acc-neat for design collaboration`
6804
7215
  ];
6805
- if (loop) lines.push(`- loop 默认模型:${loop}`);
7216
+ if (loop) lines.push(`- loop default model: ${loop}`);
6806
7217
  if (phase2) {
6807
- lines.push("- ⚠️ **Phase 2 认知对齐访谈待完成**:请按下方 5 个 Topic 逐项访谈并沉淀答案到 AGENT_SESSIONS/ 会话");
7218
+ lines.push("- ⚠️ **Phase 2 cognitive alignment interview pending**: work through the 5 Topics below and record answers in an AGENT_SESSIONS/ session");
6808
7219
  try {
6809
7220
  const prompt = readFileSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"), "utf-8");
6810
7221
  lines.push(truncateContent(prompt, Math.min(entrySkillMaxChars, 8e3)));
@@ -6895,227 +7306,48 @@ function registerContext(ctx, opts = {}) {
6895
7306
  } catch {}
6896
7307
  });
6897
7308
  if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
6898
- const { agent, messages } = payload;
6899
- const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
6900
- const key = agentKey(agent);
6901
- const downstream = await next();
6902
- if (root) {
6903
- try {
6904
- syncSafeModeRestriction(agent, root);
6905
- } catch {}
6906
- registerEntrySkillSection(agent, root);
6907
- }
6908
- if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
6909
- injected.add(key);
6910
- return {
6911
- kind: "enter",
6912
- messages: [
6913
- accMessage(root, configPaths, entrySkillMaxChars),
6914
- ...messages,
6915
- ...downstream.messages
6916
- ]
6917
- };
6918
- });
6919
- }
6920
- //#endregion
6921
- //#region src/seams/compact.ts
6922
- /**
6923
- * 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
6924
- * 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
6925
- */
6926
- function registerCompactRetention(ctx, opts = {}) {
6927
- const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
6928
- const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
6929
- ctx.on("session/event", (session, event) => {
6930
- if (event.type !== "compaction/end") return;
6931
- if (event.data.error) return;
6932
- const agent = ctx.agents.get(session.id);
6933
- if (!agent) return;
6934
- const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
6935
- if (!root) return;
6936
- try {
6937
- agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
6938
- } catch {}
6939
- });
6940
- }
6941
- //#endregion
6942
- //#region src/config-ops.ts
6943
- /**
6944
- * config-ops.ts — 高级设定配置读写(结构化,localstore.json `serenityAdvanced` 节)
6945
- *
6946
- * 与 localstore-ops 的扁平 config 节(section.key → string)不同,高级设定需要
6947
- * 结构化对象(嵌套 boolean/number/数组账号列表),故独立实现:直接读写
6948
- * localstore.json 顶层 `serenityAdvanced` 节,保留其余节(credentials/config)不变。
6949
- *
6950
- * git 策略:沿用 localstore 语义——写入时 ensureLocalstoreGitignored(deny 缺省物理保证)。
6951
- *
6952
- * 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
6953
- * (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
6954
- */
6955
- /** 高级设定节名(localstore.json 顶层) */
6956
- const ADVANCED_SECTION = "serenityAdvanced";
6957
- /** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
6958
- function defaultAdvancedSettings() {
6959
- return {
6960
- gateway: {
6961
- enabled: false,
6962
- host: "0.0.0.0",
6963
- port: 3081,
6964
- accounts: []
6965
- },
6966
- rebuild: {
6967
- enabled: true,
6968
- thresholdRatio: .9
6969
- },
6970
- naming: { enabled: true }
6971
- };
6972
- }
6973
- const SCRYPT_KEYLEN = 32;
6974
- /** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
6975
- function hashPassword(password) {
6976
- const salt = randomBytes(16);
6977
- const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
6978
- return `${salt.toString("hex")}:${derived.toString("hex")}`;
6979
- }
6980
- /** 校验密码与存储 hash(timing-safe) */
6981
- function verifyPassword(password, stored) {
6982
- const idx = stored.indexOf(":");
6983
- if (idx <= 0) return false;
6984
- const salt = Buffer.from(stored.slice(0, idx), "hex");
6985
- const expected = Buffer.from(stored.slice(idx + 1), "hex");
6986
- const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
6987
- return expected.length === derived.length && timingSafeEqual(expected, derived);
6988
- }
6989
- function readAll(root) {
6990
- const p = localstorePath(root);
6991
- if (!existsSync(p)) return {};
6992
- try {
6993
- const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
6994
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
6995
- return {};
6996
- } catch {
6997
- return {};
6998
- }
6999
- }
7000
- function writeAll(root, data) {
7001
- writeFileSync(localstorePath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
7002
- ensureLocalstoreGitignored(root);
7003
- }
7004
- /** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
7005
- function mergeWithDefaults(raw) {
7006
- const def = defaultAdvancedSettings();
7007
- if (raw === null || typeof raw !== "object") return def;
7008
- const o = raw;
7009
- const gateway = o.gateway ?? {};
7010
- const rebuild = o.rebuild ?? {};
7011
- const naming = o.naming ?? {};
7012
- return {
7013
- gateway: {
7014
- enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
7015
- host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
7016
- port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
7017
- accounts: Array.isArray(gateway.accounts) ? gateway.accounts.filter((a) => typeof a === "object" && a !== null && typeof a.id === "string" && typeof a.user === "string" && typeof a.passHash === "string") : def.gateway.accounts
7018
- },
7019
- rebuild: {
7020
- enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
7021
- thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
7022
- },
7023
- naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled }
7024
- };
7025
- }
7026
- /** 读取高级设定(文件缺失/坏 JSON → 默认值) */
7027
- function readAdvancedSettings(root) {
7028
- return mergeWithDefaults(readAll(root)[ADVANCED_SECTION]);
7029
- }
7030
- /** 写入高级设定全量(保留其他节) */
7031
- function writeAdvancedSettings(root, settings) {
7032
- const all = readAll(root);
7033
- all[ADVANCED_SECTION] = settings;
7034
- writeAll(root, all);
7035
- }
7036
- /**
7037
- * 部分更新(面板 PUT 用):传入 Partial,深合并到现有值。
7038
- * accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
7039
- */
7040
- function updateAdvancedSettings(root, patch) {
7041
- const current = readAdvancedSettings(root);
7042
- const gw = patch.gateway;
7043
- const rb = patch.rebuild;
7044
- const nm = patch.naming;
7045
- const next = {
7046
- gateway: gw !== void 0 ? {
7047
- enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
7048
- host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
7049
- port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
7050
- accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts
7051
- } : current.gateway,
7052
- rebuild: rb !== void 0 ? {
7053
- enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
7054
- thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
7055
- } : current.rebuild,
7056
- naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming
7057
- };
7058
- writeAdvancedSettings(root, next);
7059
- return next;
7060
- }
7061
- /** 持久化 → wire(剥离 passHash;只留 hasPassword 布尔) */
7062
- function toWire(settings) {
7063
- return {
7064
- gateway: {
7065
- enabled: settings.gateway.enabled,
7066
- host: settings.gateway.host,
7067
- port: settings.gateway.port,
7068
- accounts: settings.gateway.accounts.map((a) => ({
7069
- id: a.id,
7070
- user: a.user,
7071
- hasPassword: a.passHash !== ""
7072
- }))
7073
- },
7074
- rebuild: settings.rebuild,
7075
- naming: settings.naming
7076
- };
7309
+ const { agent, messages } = payload;
7310
+ const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
7311
+ const key = agentKey(agent);
7312
+ const downstream = await next();
7313
+ if (root) {
7314
+ try {
7315
+ syncSafeModeRestriction(agent, root);
7316
+ } catch {}
7317
+ registerEntrySkillSection(agent, root);
7318
+ }
7319
+ if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
7320
+ injected.add(key);
7321
+ return {
7322
+ kind: "enter",
7323
+ messages: [
7324
+ accMessage(root, configPaths, entrySkillMaxChars),
7325
+ ...messages,
7326
+ ...downstream.messages
7327
+ ]
7328
+ };
7329
+ });
7077
7330
  }
7331
+ //#endregion
7332
+ //#region src/seams/compact.ts
7078
7333
  /**
7079
- * wire → 持久化(面板 PUT 用):
7080
- * accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
7081
- * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
7334
+ * 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
7335
+ * 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
7082
7336
  */
7083
- function applyWirePatch(root, wire) {
7084
- const current = readAdvancedSettings(root);
7085
- const patch = {};
7086
- if (wire.gateway !== void 0) {
7087
- const gwPatch = { ...current.gateway };
7088
- if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
7089
- if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
7090
- if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
7091
- if (Array.isArray(wire.gateway.accounts)) {
7092
- const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
7093
- gwPatch.accounts = wire.gateway.accounts.map((a) => {
7094
- const existing = byId.get(a.id);
7095
- const pass = a.pass;
7096
- if (typeof pass === "string" && pass !== "") return {
7097
- id: a.id,
7098
- user: a.user,
7099
- passHash: hashPassword(pass)
7100
- };
7101
- if (existing) return {
7102
- id: a.id,
7103
- user: a.user,
7104
- passHash: existing.passHash
7105
- };
7106
- throw new Error(`账号 "${a.user}"(id=${a.id})未提供密码且无现有 hash——新账号必须设置密码`);
7107
- });
7108
- }
7109
- patch.gateway = gwPatch;
7110
- }
7111
- if (wire.rebuild !== void 0) {
7112
- const rbPatch = { ...current.rebuild };
7113
- if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
7114
- if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
7115
- patch.rebuild = rbPatch;
7116
- }
7117
- if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
7118
- return updateAdvancedSettings(root, patch);
7337
+ function registerCompactRetention(ctx, opts = {}) {
7338
+ const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
7339
+ const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
7340
+ ctx.on("session/event", (session, event) => {
7341
+ if (event.type !== "compaction/end") return;
7342
+ if (event.data.error) return;
7343
+ const agent = ctx.agents.get(session.id);
7344
+ if (!agent) return;
7345
+ const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
7346
+ if (!root) return;
7347
+ try {
7348
+ agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
7349
+ } catch {}
7350
+ });
7119
7351
  }
7120
7352
  //#endregion
7121
7353
  //#region src/api.ts
@@ -7155,7 +7387,7 @@ function saveImageToTmp(root, mediaType, data) {
7155
7387
  writeFileSync(join(dir, filename), bytes);
7156
7388
  return `${IMAGE_UPLOAD_DIR}/${filename}`;
7157
7389
  }
7158
- function readBody(req, maxBytes) {
7390
+ function readBody$1(req, maxBytes) {
7159
7391
  return new Promise((resolve, reject) => {
7160
7392
  let data = "";
7161
7393
  req.on("data", (chunk) => {
@@ -7177,14 +7409,27 @@ function sendJson(res, code, body) {
7177
7409
  });
7178
7410
  res.end(payload);
7179
7411
  }
7180
- /** 从请求参数解析 workspace:优先 sessionId → 会话 header.cwd;其次 workspace 参数;最后进程 cwd */
7412
+ /**
7413
+ * 从请求参数解析 workspace:优先 sessionId → 会话 header.cwd;其次 workspace 参数;
7414
+ * 无参 → 遍历 live sessions 找第一个 CCC 会话的 cwd(远程访问时 sessionId 可能缺失,
7415
+ * 回退 process.cwd()=$HOME 会错误显示"未激活"——v1.22.2 修复);最后进程 cwd。
7416
+ * 纯逻辑(可单测):resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds)。
7417
+ */
7418
+ function resolveWorkspaceCore(sessionCwd, workspaceParam, listCwds, fallback) {
7419
+ if (sessionCwd) return sessionCwd;
7420
+ if (workspaceParam) return workspaceParam;
7421
+ for (const cwd of listCwds()) if (cwd && findSerenityRoot(cwd)) return cwd;
7422
+ return fallback;
7423
+ }
7181
7424
  function resolveWorkspace(ctx, params) {
7182
- if (params.sessionId) {
7183
- const session = ctx.sessions?.get?.(params.sessionId);
7184
- if (session?.header?.cwd) return session.header.cwd;
7185
- }
7186
- if (typeof params.workspace === "string" && params.workspace) return params.workspace;
7187
- return process.cwd();
7425
+ let sessionCwd;
7426
+ if (params.sessionId) sessionCwd = (ctx.sessions?.get?.(params.sessionId))?.header?.cwd;
7427
+ let listCwds = () => [];
7428
+ try {
7429
+ const sessions = ctx.sessions;
7430
+ if (typeof sessions?.list === "function") listCwds = () => sessions.list().map((s) => s.header?.cwd ?? "").filter(Boolean);
7431
+ } catch {}
7432
+ return resolveWorkspaceCore(sessionCwd, params.workspace, listCwds, process.cwd());
7188
7433
  }
7189
7434
  function registerStatusApi(ctx, opts = {}) {
7190
7435
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
@@ -7225,7 +7470,7 @@ function registerStatusApi(ctx, opts = {}) {
7225
7470
  sendJson(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
7226
7471
  return;
7227
7472
  }
7228
- const raw = await readBody(req, 20971520);
7473
+ const raw = await readBody$1(req, 20971520);
7229
7474
  const body = JSON.parse(raw);
7230
7475
  const workspace = resolveWorkspace(ctx, {
7231
7476
  sessionId: body.sessionId,
@@ -7268,7 +7513,7 @@ function registerStatusApi(ctx, opts = {}) {
7268
7513
  sendJson(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
7269
7514
  return;
7270
7515
  }
7271
- const raw = await readBody(req, 65536);
7516
+ const raw = await readBody$1(req, 65536);
7272
7517
  const body = JSON.parse(raw);
7273
7518
  const workspace = resolveWorkspace(ctx, {
7274
7519
  sessionId: body.sessionId,
@@ -7302,35 +7547,12 @@ function registerStatusApi(ctx, opts = {}) {
7302
7547
  return;
7303
7548
  }
7304
7549
  if (req.method === "GET") {
7305
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
7306
- const workspace = resolveWorkspace(ctx, {
7307
- sessionId: url.searchParams.get("sessionId") ?? void 0,
7308
- workspace: url.searchParams.get("workspace") ?? void 0
7309
- });
7310
- const root = findSerenityRoot(workspace);
7311
- if (!root) {
7312
- sendJson(res, 200, {
7313
- config: null,
7314
- error: `no CCC found from workspace: ${workspace}`
7315
- });
7316
- return;
7317
- }
7318
- sendJson(res, 200, { config: toWire(readAdvancedSettings(root)) });
7550
+ sendJson(res, 200, { config: toWire(readAdvancedSettings()) });
7319
7551
  return;
7320
7552
  }
7321
7553
  if (req.method === "PUT") {
7322
- const raw = await readBody(req, 131072);
7323
- const body = JSON.parse(raw);
7324
- const workspace = resolveWorkspace(ctx, {
7325
- sessionId: body.sessionId,
7326
- workspace: body.workspace
7327
- });
7328
- const root = findSerenityRoot(workspace);
7329
- if (!root) {
7330
- sendJson(res, 404, { error: `no CCC found from workspace: ${workspace}` });
7331
- return;
7332
- }
7333
- const saved = applyWirePatch(root, body.config ?? {});
7554
+ const raw = await readBody$1(req, 131072);
7555
+ const saved = applyWirePatch(JSON.parse(raw).config ?? {});
7334
7556
  try {
7335
7557
  ctx.emit?.("serenity/config-updated");
7336
7558
  } catch {}
@@ -7360,9 +7582,9 @@ function registerEnv(ctx) {
7360
7582
  ctx.shellEnv.register({
7361
7583
  name: "dsh-serenity-hooks",
7362
7584
  variables: {
7363
- DSH_SERENITY_ROOT: { description: "CCC 根目录(宁静号认知容器)" },
7364
- DSH_SERENITY_CCC: { description: "CCC 名称(根目录 basename)" },
7365
- DSH_SERENITY_VERSION: { description: "ACC 插件版本" }
7585
+ DSH_SERENITY_ROOT: { description: "CCC root directory (Serenity cognitive container)" },
7586
+ DSH_SERENITY_CCC: { description: "CCC name (root directory basename)" },
7587
+ DSH_SERENITY_VERSION: { description: "ACC plugin version" }
7366
7588
  },
7367
7589
  resolve(execution) {
7368
7590
  return resolveSerenityEnv((execution.agent?.session)?.header?.cwd ?? process.cwd());
@@ -7482,41 +7704,126 @@ function registerOpencodeSkills(ctx) {
7482
7704
  ctx.skills.registerProvider(() => provider);
7483
7705
  }
7484
7706
  //#endregion
7485
- //#region src/gateway.ts
7486
- /** 极简登录页(内嵌 HTML,无外部资源——适配任何部署) */
7487
- function loginPageHtml(extra) {
7488
- return `<!doctype html><html lang="zh"><head><meta charset="utf-8"><title>Serenity 登录</title>
7489
- <style>
7490
- body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#111;color:#eee}
7491
- .card{background:#1c1c1e;padding:32px 40px;border-radius:12px;width:300px;box-shadow:0 8px 30px rgba(0,0,0,.5)}
7492
- h1{font-size:18px;margin:0 0 20px}input{width:100%;box-sizing:border-box;padding:10px;margin:8px 0;border:1px solid #333;border-radius:6px;background:#222;color:#eee}
7493
- button{width:100%;padding:10px;margin-top:16px;border:0;border-radius:6px;background:#3b82f6;color:#fff;font-size:14px;cursor:pointer}
7494
- .error{color:#f87171;font-size:12px;min-height:16px;margin-top:8px}
7495
- </style></head><body><div class="card"><h1>🔐 Serenity Web UI</h1>
7496
- <form method="post" action="/serenity/login">
7497
- <input type="text" name="user" placeholder="用户名" autocomplete="username" required autofocus>
7498
- <input type="password" name="password" placeholder="密码" autocomplete="current-password" required>
7499
- <button type="submit">登录</button>
7500
- <div class="error">${extra}</div>
7501
- </form></div></body></html>`;
7502
- }
7503
- /** 账号验证(纯逻辑):user + password 对 localstore accounts 匹配(scrypt) */
7707
+ //#region src/gateway-auth.ts
7708
+ /**
7709
+ * gateway-auth.ts — F1 双端口网关:认证域纯逻辑(v1.22.4 安全加固)
7710
+ *
7711
+ * 从 gateway.ts 拆分(S142 熵点治理,v1.22.8):认证纯逻辑独立成模块——
7712
+ * 登录验证(scrypt)/ 会话 token(滑动 TTL + 吊销)/ 失败锁定(账号维度指数退避)/
7713
+ * CSRF(双提交 + Origin 校验)/ cookie 解析 / 登录页 HTML。
7714
+ * gateway.ts 保留 HTTP 装配(startGateway/registerGateway)并从本模块 import。
7715
+ *
7716
+ * 纯函数设计(可单测):无 socket/无 HTTP 依赖,全部导出可注入测试。
7717
+ */
7504
7718
  function verifyGatewayLogin(accounts, user, password) {
7505
7719
  const account = accounts.find((a) => a.user === user);
7506
7720
  if (!account) return false;
7507
7721
  return verifyPassword(password, account.passHash);
7508
7722
  }
7509
- /** 内存 token 表(重启即清空——用户决策) */
7510
- const tokens = /* @__PURE__ */ new Set();
7511
- /** 颁发 token(Hex 32 字节) */
7512
- function issueToken() {
7723
+ /** 内存会话表(重启即清空——用户决策) */
7724
+ const sessions = /* @__PURE__ */ new Map();
7725
+ /** 颁发会话 token(Hex 32 字节) */
7726
+ function issueToken(user) {
7513
7727
  const token = randomBytes(32).toString("hex");
7514
- tokens.add(token);
7728
+ sessions.set(token, {
7729
+ token,
7730
+ lastActiveAt: Date.now(),
7731
+ user
7732
+ });
7515
7733
  return token;
7516
7734
  }
7517
- /** 校验 token 是否有效 */
7735
+ /** 主动吊销会话(登出);返回是否确实存在 */
7736
+ function revokeToken(token) {
7737
+ return sessions.delete(token);
7738
+ }
7739
+ /**
7740
+ * 校验 token 有效性(滑动过期:有效则刷新 lastActiveAt)。
7741
+ * @returns 有效会话或 undefined
7742
+ */
7518
7743
  function validateToken(token) {
7519
- return typeof token === "string" && token !== "" && tokens.has(token);
7744
+ if (typeof token !== "string" || token === "") return void 0;
7745
+ const session = sessions.get(token);
7746
+ if (session === void 0) return void 0;
7747
+ if (Date.now() - session.lastActiveAt > 864e5) {
7748
+ sessions.delete(token);
7749
+ return;
7750
+ }
7751
+ session.lastActiveAt = Date.now();
7752
+ return session;
7753
+ }
7754
+ /** 首次锁定时长(毫秒) */
7755
+ const FAIL_LOCK_BASE_MS = 9e5;
7756
+ /** 锁定时长指数退避上限 */
7757
+ const FAIL_LOCK_MAX_MS = 144e5;
7758
+ /** 账号 → 失败状态(内存;重启清零——攻击者重启服务可绕过,但公网常驻服务下有效) */
7759
+ const failStates = /* @__PURE__ */ new Map();
7760
+ /** 获取账号失败状态(纯逻辑,供测试注入) */
7761
+ function getFailState(user) {
7762
+ let st = failStates.get(user);
7763
+ if (!st) {
7764
+ st = {
7765
+ count: 0,
7766
+ lockedUntil: 0,
7767
+ lockRound: 0
7768
+ };
7769
+ failStates.set(user, st);
7770
+ }
7771
+ return st;
7772
+ }
7773
+ /** 重置失败状态(登录成功/管理员解封) */
7774
+ function resetFailState(user) {
7775
+ failStates.delete(user);
7776
+ }
7777
+ /** 记录一次失败;达到阈值 → 锁定(指数退避)。返回锁定剩余毫秒(0 = 未锁定) */
7778
+ function recordLoginFailure(user) {
7779
+ const st = getFailState(user);
7780
+ if (st.lockedUntil > 0 && Date.now() < st.lockedUntil) return st.lockedUntil - Date.now();
7781
+ st.count += 1;
7782
+ if (st.count >= 5) {
7783
+ const base = FAIL_LOCK_BASE_MS * 2 ** st.lockRound;
7784
+ st.lockedUntil = Date.now() + Math.min(base, FAIL_LOCK_MAX_MS);
7785
+ st.lockRound += 1;
7786
+ st.count = 0;
7787
+ return st.lockedUntil - Date.now();
7788
+ }
7789
+ return 0;
7790
+ }
7791
+ /** 剩余锁定毫秒(0 = 未锁定) */
7792
+ function accountLockRemaining(user) {
7793
+ const st = getFailState(user);
7794
+ return st.lockedUntil > 0 && Date.now() < st.lockedUntil ? st.lockedUntil - Date.now() : 0;
7795
+ }
7796
+ /** 生成 CSRF cookie 值(随机 32B hex) */
7797
+ function newCsrfToken() {
7798
+ return randomBytes(32).toString("hex");
7799
+ }
7800
+ /** 从请求头取 CSRF token(X-CSRF-Token 或表单字段) */
7801
+ function csrfFromRequest(req, body) {
7802
+ const header = req.headers["x-csrf-token"];
7803
+ if (typeof header === "string" && header !== "") return header;
7804
+ const form = body.get("csrf");
7805
+ return form && form !== "" ? form : null;
7806
+ }
7807
+ /** 常量时间比较(token 校验用) */
7808
+ function safeEqual(a, b) {
7809
+ const ab = Buffer.from(a);
7810
+ const bb = Buffer.from(b);
7811
+ return ab.length === bb.length && timingSafeEqual(ab, bb);
7812
+ }
7813
+ /**
7814
+ * Origin 校验(S3):跨站 POST 被浏览器强制带 Origin(同源 GET/导航除外)。
7815
+ * 允许:① 同源(Origin.host === 网关自身 host)② 主端口 loopback(127.0.0.1:mainPort)。
7816
+ * 无 Origin(curl/非浏览器直连)→ 仅当携带有效 CSRF 双提交时放行(脚本无 cookie 无法伪造)。
7817
+ * @returns 是否通过
7818
+ */
7819
+ function originAllowed(originHeader, reqHost, mainPort) {
7820
+ if (originHeader === void 0 || originHeader === "null") return true;
7821
+ try {
7822
+ const originHost = new URL(originHeader).host;
7823
+ return originHost === reqHost || originHost === `127.0.0.1:${mainPort}` || originHost === `localhost:${mainPort}`;
7824
+ } catch {
7825
+ return false;
7826
+ }
7520
7827
  }
7521
7828
  /** 从 Cookie 头解析指定 cookie 值(纯逻辑,可单测) */
7522
7829
  function cookieValue(header, name) {
@@ -7528,43 +7835,391 @@ function cookieValue(header, name) {
7528
7835
  }
7529
7836
  }
7530
7837
  /**
7531
- * 启动第二监听器。返回 { server, dispose }——dispose 关 server + 清 token。
7838
+ * 登录页:内嵌 HTML 无外部资源——适配任何部署。
7839
+ * 移动端关键点:
7840
+ * - viewport meta(防止移动浏览器按 980px 视口缩放)
7841
+ * - 触控目标 ≥ 44px(Apple HIG);输入字号 ≥ 16px(iOS 聚焦不自动放大)
7842
+ * - env(safe-area-inset-*) 适配刘海屏/底部手势条
7843
+ * - color-scheme: dark + 明暗自适应
7844
+ * @param extra - 错误提示文案
7845
+ * @param csrf - CSRF token(隐藏字段随表单双提交;无 = 旧调用方兼容)
7846
+ */
7847
+ function loginPageHtml(extra, csrf) {
7848
+ return `<!doctype html><html lang="zh"><head><meta charset="utf-8">
7849
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
7850
+ <meta name="color-scheme" content="dark light">
7851
+ <meta name="theme-color" content="#111114">
7852
+ <meta name="referrer" content="no-referrer">
7853
+ <title>Serenity 登录</title>
7854
+ <style>
7855
+ :root{color-scheme:dark}
7856
+ *{box-sizing:border-box}
7857
+ html,body{height:100%}
7858
+ body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;display:flex;align-items:center;justify-content:center;margin:0;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);background:#111114;color:#eee}
7859
+ .card{background:#1c1c20;padding:32px 28px;border-radius:16px;width:min(340px,calc(100vw - 48px));box-shadow:0 12px 40px rgba(0,0,0,.55);border:1px solid rgba(255,255,255,.06)}
7860
+ h1{font-size:20px;margin:0 0 4px;text-align:center;letter-spacing:.02em}
7861
+ .sub{font-size:13px;color:#999;text-align:center;margin:0 0 24px}
7862
+ label{display:block;font-size:13px;color:#bbb;margin:14px 0 6px}
7863
+ input{width:100%;padding:14px 14px;margin:0;border:1px solid #3a3a40;border-radius:10px;background:#141418;color:#eee;font-size:16px;-webkit-appearance:none;appearance:none}
7864
+ input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.25)}
7865
+ input::placeholder{color:#666}
7866
+ #f-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.35em;text-align:center;font-weight:600}
7867
+ button{width:100%;padding:15px 14px;margin-top:24px;border:0;border-radius:10px;background:#3b82f6;color:#fff;font-size:16px;font-weight:600;cursor:pointer;min-height:50px;-webkit-appearance:none;appearance:none}
7868
+ button:active{background:#2563eb}
7869
+ .error{color:#f87171;font-size:13px;min-height:18px;margin-top:12px;text-align:center}
7870
+ .hint{color:#777;font-size:12px;margin-top:10px;text-align:center}
7871
+ .foot{margin-top:20px;text-align:center;font-size:12px;color:#666}
7872
+ @media (prefers-color-scheme:light){
7873
+ body{background:#f4f4f6;color:#1a1a1e}
7874
+ .card{background:#fff;border-color:rgba(0,0,0,.08);box-shadow:0 12px 40px rgba(0,0,0,.12)}
7875
+ .sub{color:#666}
7876
+ label{color:#444}
7877
+ input{background:#fafafa;border-color:#d0d0d6;color:#1a1a1e}
7878
+ input::placeholder{color:#aaa}
7879
+ .hint{color:#999}
7880
+ .foot{color:#999}
7881
+ }
7882
+ </style></head><body><div class="card">
7883
+ <h1>🔐 Serenity Web UI</h1>
7884
+ <p class="sub">宁静号 · 外部访问</p>
7885
+ <form method="post" action="/serenity/login">
7886
+ ${csrf ? `<input type="hidden" name="csrf" value="${csrf}">` : ""}
7887
+ <label for="f-user">用户名</label>
7888
+ <input id="f-user" type="text" name="user" placeholder="用户名" autocomplete="username" autocapitalize="none" autocorrect="off" required autofocus enterkeyhint="next">
7889
+ <label for="f-pass">密码</label>
7890
+ <input id="f-pass" type="password" name="password" placeholder="密码" autocomplete="current-password" required enterkeyhint="next">
7891
+ <label for="f-code">验证码(已绑定 Authenticator 时填写)</label>
7892
+ <input id="f-code" type="text" name="code" placeholder="6 位验证码" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" enterkeyhint="go">
7893
+ <button type="submit">登录</button>
7894
+ <div class="error">${extra}</div>
7895
+ <p class="hint">未绑定验证器的账号只需用户名 + 密码</p>
7896
+ </form>
7897
+ <p class="foot">Serenity ACC · dsh-serenity-hooks</p>
7898
+ </div></body></html>`;
7899
+ }
7900
+ //#endregion
7901
+ //#region src/gateway-proxy.ts
7902
+ /**
7903
+ * gateway-proxy.ts — F1 双端口网关:代理辅助纯逻辑(v1.22.1-1.22.3 稳定性修复)
7904
+ *
7905
+ * 从 gateway.ts 拆分(S142 熵点治理,v1.22.8):
7906
+ * - crypto.randomUUID polyfill(非安全上下文修复,v1.22.0)
7907
+ * - 反代请求头构造(信任栅栏修复:Host + Origin 同步改写 loopback,v1.22.1)
7908
+ * - workspace 白名单过滤(响应 items 前缀过滤 + create 校验 + 403 RPC 响应,v1.22)
7909
+ * - HTML polyfill 注入(幂等 marker)
7910
+ * 纯函数设计(可单测);gateway.ts 装配层 import 使用。
7911
+ */
7912
+ /**
7913
+ * 经第二端口 http://LAN-IP:3081 访问 = 非安全上下文 → DSH client 的
7914
+ * `crypto.randomUUID()`(ui-conversation/service.ts 等)抛错,provider 目录加载失败。
7915
+ * 用 `crypto.getRandomValues` 实现(与 DSH 官方 random-uuid.ts 同算法),
7916
+ * 零改 DSH——gateway 反代 HTML 时注入。
7917
+ */
7918
+ const RANDOM_UUID_POLYFILL = `<script>
7919
+ (function () {
7920
+ if (typeof crypto === 'undefined' || typeof crypto.randomUUID === 'function') return
7921
+ try {
7922
+ crypto.randomUUID = function () {
7923
+ var bytes = crypto.getRandomValues(new Uint8Array(16))
7924
+ bytes[6] = (bytes[6] & 0x0f) | 0x40
7925
+ bytes[8] = (bytes[8] & 0x3f) | 0x80
7926
+ var hex = Array.from(bytes, function (b) { return b.toString(16).padStart(2, '0') }).join('')
7927
+ return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20)
7928
+ }
7929
+ } catch (e) { /* getRandomValues 也不可用则放弃 */ }
7930
+ })()
7931
+ <\/script>`;
7932
+ /** 注入标记(幂等:已注入的 HTML 不重复注入) */
7933
+ const POLYFILL_MARKER = "data-sp-randomuuid-polyfill";
7934
+ /**
7935
+ * workspace.list 响应过滤(v1.22 白名单):
7936
+ * DSH client→server RPC 全部走 HTTP JSON(`POST /api/workspace.list`,WS 仅下行推送)。
7937
+ * 白名单(workspaces 路径前缀)非空 → 只保留匹配前缀的 items;
7938
+ * 空 = 全部允许(默认,向后兼容)。
7939
+ */
7940
+ function filterWorkspaceList(body, allowPrefixes) {
7941
+ if (allowPrefixes.length === 0) return body;
7942
+ try {
7943
+ const parsed = JSON.parse(body);
7944
+ const value = parsed?.result?.value;
7945
+ if (parsed?.result?.ok !== true || !value || !Array.isArray(value.items)) return body;
7946
+ const keep = (path) => typeof path === "string" && allowPrefixes.some((p) => path.startsWith(p));
7947
+ value.items = value.items.filter((item) => keep(item.path));
7948
+ return JSON.stringify(parsed);
7949
+ } catch {
7950
+ return body;
7951
+ }
7952
+ }
7953
+ /**
7954
+ * 校验 workspace.create 请求路径是否在白名单内(v1.22):
7955
+ * 白名单非空且路径不匹配 → 拒绝(由调用方构造 403 RPC 响应)。
7956
+ */
7957
+ function workspaceAllowed(allowPrefixes, path) {
7958
+ if (allowPrefixes.length === 0) return true;
7959
+ return typeof path === "string" && allowPrefixes.some((p) => path.startsWith(p));
7960
+ }
7961
+ /** 构造 workspace.create 拒绝的 JSON RPC 响应体(code=forbidden) */
7962
+ function workspaceDenyResponse(rpcId) {
7963
+ return JSON.stringify({
7964
+ type: "server-response",
7965
+ rpcId,
7966
+ result: {
7967
+ ok: false,
7968
+ error: {
7969
+ code: "forbidden",
7970
+ message: "workspace not in external allowlist",
7971
+ details: {}
7972
+ }
7973
+ }
7974
+ });
7975
+ }
7976
+ /** 在 HTML 的 </head> 前注入 polyfill(幂等:含 marker 则跳过) */
7977
+ function injectPolyfillHtml(html) {
7978
+ if (html.includes(POLYFILL_MARKER)) return html;
7979
+ const head = RANDOM_UUID_POLYFILL.replace("<script>", `<script ${POLYFILL_MARKER}="1">`);
7980
+ if (html.includes("</head>")) return html.replace("</head>", `${head}\n</head>`);
7981
+ return `${head}\n${html}`;
7982
+ }
7983
+ /**
7984
+ * 反代请求头构造(v1.22.1 信任栅栏修复,纯逻辑可测):
7985
+ * DSH isTrustedApiRequest 要求 Origin.host === Host.host——Host 改写为 loopback 后
7986
+ * Origin 必须同步改写(浏览器 POST 必带 Origin,透传外部地址 → 403)。
7987
+ */
7988
+ function buildProxyHeaders(reqHeaders, mainPort, bodyOverride) {
7989
+ const headers = {
7990
+ ...reqHeaders,
7991
+ host: `127.0.0.1:${mainPort}`,
7992
+ origin: `http://127.0.0.1:${mainPort}`
7993
+ };
7994
+ if (bodyOverride !== void 0) headers["content-length"] = Buffer.byteLength(bodyOverride);
7995
+ return headers;
7996
+ }
7997
+ //#endregion
7998
+ //#region src/gateway.ts
7999
+ /** 读取请求体(≤ maxBytes;超限 reject) */
8000
+ function readBody(req, maxBytes) {
8001
+ return new Promise((resolve, reject) => {
8002
+ let data = "";
8003
+ req.on("data", (chunk) => {
8004
+ data += chunk.toString("utf-8");
8005
+ if (data.length > maxBytes) {
8006
+ reject(/* @__PURE__ */ new Error("body too large"));
8007
+ req.destroy();
8008
+ }
8009
+ });
8010
+ req.on("end", () => resolve(data));
8011
+ req.on("error", reject);
8012
+ });
8013
+ }
8014
+ /**
8015
+ * 启动第二监听器。返回 { server, dispose }——dispose 关 server(**不清 token**:
8016
+ * token 模块级,进程重启自然清空;热重建清 token → 已登录用户 WS 断 + 重连 cookie 无效)。
7532
8017
  * @param config - 监听/代理配置。
7533
- * @param getAccounts - 运行时读取账号列表(localstore;gateway.enabled 开关在调用方判)。
8018
+ * @param getAccounts - 运行时读取账号列表(plugin 全局文件;gateway.enabled 开关在调用方判)。
7534
8019
  */
7535
8020
  function startGateway(config, getAccounts) {
7536
8021
  const { host, port, cookieName, mainPort, loginDelayMs } = config;
7537
- /** 校验请求 Cookie 是否含有效 token */
8022
+ const allowWorkspaces = config.allowWorkspaces ?? [];
8023
+ const cookieSecure = config.cookieSecure === true;
8024
+ const allowWorkspaceCreate = config.allowWorkspaceCreate !== false;
8025
+ const totpEnabled = config.totpEnabled === true;
8026
+ /** 校验请求 Cookie 是否含有效 token(v1.22.4:滑动过期 + 返回会话供审计) */
7538
8027
  const authed = (req) => validateToken(cookieValue(req.headers.cookie, cookieName));
7539
- /** 反向代理:改写 Host 头 → 主端口(loopback 过信任栅栏) */
7540
- const proxy = (req, res) => {
8028
+ /**
8029
+ * 反向代理:改写 Host + Origin 头 → 主端口(loopback 过信任栅栏)。
8030
+ * v1.22.1 修复:DSH 信任栅栏(api-request-trust.ts isTrustedApiRequest)要求
8031
+ * **Origin.host === Host.host**——仅改写 Host(127.0.0.1:3080)而 Origin 透传
8032
+ * (http://192.168.x.x:3081)→ 不一致 → 所有 /api RPC 403(host.pickDirectory 首个暴露)。
8033
+ * 增强:
8034
+ * - HTML 响应 → 注入 crypto.randomUUID polyfill(非安全上下文修复)
8035
+ * - /api/workspace.list 响应 → 白名单过滤 items
8036
+ * @param bodyOverride - workspace.create 已读 body 时的重放(白名单检查后转发)
8037
+ */
8038
+ const proxy = (req, res, bodyOverride) => {
8039
+ const url = new URL(req.url ?? "/", `http://${host}:${port}`);
8040
+ const method = req.method === "POST" && url.pathname.startsWith("/api/") ? url.pathname.slice(5) : null;
8041
+ const headers = buildProxyHeaders(req.headers, mainPort, bodyOverride);
7541
8042
  const target = request({
7542
8043
  host: "127.0.0.1",
7543
8044
  port: mainPort,
7544
8045
  path: req.url ?? "/",
7545
8046
  method: req.method,
7546
- headers: {
7547
- ...req.headers,
7548
- host: `127.0.0.1:${mainPort}`
7549
- }
8047
+ headers
7550
8048
  }, (upstream) => {
7551
- res.writeHead(upstream.statusCode ?? 502, upstream.headers);
8049
+ const status = upstream.statusCode ?? 502;
8050
+ const ct = String(upstream.headers["content-type"] ?? "");
8051
+ if (status === 200 && ct.includes("text/html")) {
8052
+ const chunks = [];
8053
+ upstream.on("data", (c) => chunks.push(c));
8054
+ upstream.on("end", () => {
8055
+ const transformed = injectPolyfillHtml(Buffer.concat(chunks).toString("utf-8"));
8056
+ const out = {
8057
+ ...upstream.headers,
8058
+ "content-length": Buffer.byteLength(transformed)
8059
+ };
8060
+ res.writeHead(status, out);
8061
+ res.end(transformed);
8062
+ });
8063
+ upstream.on("error", () => {
8064
+ try {
8065
+ res.destroy();
8066
+ } catch {}
8067
+ });
8068
+ return;
8069
+ }
8070
+ if (method === "workspace.list" && status === 200 && ct.includes("application/json")) {
8071
+ const chunks = [];
8072
+ upstream.on("data", (c) => chunks.push(c));
8073
+ upstream.on("end", () => {
8074
+ const transformed = filterWorkspaceList(Buffer.concat(chunks).toString("utf-8"), allowWorkspaces);
8075
+ const out = {
8076
+ ...upstream.headers,
8077
+ "content-length": Buffer.byteLength(transformed)
8078
+ };
8079
+ res.writeHead(status, out);
8080
+ res.end(transformed);
8081
+ });
8082
+ upstream.on("error", () => {
8083
+ try {
8084
+ res.destroy();
8085
+ } catch {}
8086
+ });
8087
+ return;
8088
+ }
8089
+ res.writeHead(status, upstream.headers);
7552
8090
  upstream.pipe(res);
8091
+ upstream.on("error", () => {
8092
+ try {
8093
+ res.destroy();
8094
+ } catch {}
8095
+ });
7553
8096
  });
7554
8097
  target.on("error", () => {
7555
- res.writeHead(502, { "content-type": "text/plain" });
7556
- res.end("502 Bad Gateway: dsh web 主端口不可达");
8098
+ try {
8099
+ res.writeHead(502, { "content-type": "text/plain" });
8100
+ res.end("502 Bad Gateway: dsh web 主端口不可达");
8101
+ } catch {}
8102
+ });
8103
+ req.on("error", () => {
8104
+ try {
8105
+ target.destroy();
8106
+ } catch {}
7557
8107
  });
7558
- req.pipe(target);
8108
+ res.on("error", () => {
8109
+ try {
8110
+ target.destroy();
8111
+ } catch {}
8112
+ });
8113
+ if (bodyOverride !== void 0) target.end(bodyOverride);
8114
+ else req.pipe(target);
7559
8115
  };
8116
+ const lastTotpCounter = /* @__PURE__ */ new Map();
7560
8117
  const server = createServer((req, res) => {
7561
8118
  const url = new URL(req.url ?? "/", `http://${host}:${port}`);
7562
- if (authed(req)) {
8119
+ if (req.method === "POST" && url.pathname === "/serenity/logout") {
8120
+ const cookie = cookieValue(req.headers.cookie, cookieName);
8121
+ if (cookie !== void 0) revokeToken(cookie);
8122
+ res.writeHead(302, {
8123
+ location: "/",
8124
+ "set-cookie": `${cookieName}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${cookieSecure ? "; Secure" : ""}`
8125
+ });
8126
+ res.end();
8127
+ return;
8128
+ }
8129
+ const session = authed(req);
8130
+ if (session !== void 0) {
8131
+ if (req.method === "POST" && url.pathname === "/api/workspace.create") {
8132
+ if (!allowWorkspaceCreate) {
8133
+ readBody(req, 131072).then((body) => {
8134
+ let rpcId = "unknown";
8135
+ try {
8136
+ const parsed = JSON.parse(body);
8137
+ if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
8138
+ } catch {}
8139
+ res.writeHead(403, { "content-type": "application/json" });
8140
+ res.end(JSON.stringify({
8141
+ type: "server-response",
8142
+ rpcId,
8143
+ result: {
8144
+ ok: false,
8145
+ error: {
8146
+ code: "forbidden",
8147
+ message: "workspace creation disabled for external access",
8148
+ details: {}
8149
+ }
8150
+ }
8151
+ }));
8152
+ }).catch(() => {
8153
+ res.writeHead(400, { "content-type": "application/json" });
8154
+ res.end(JSON.stringify({
8155
+ type: "server-response",
8156
+ rpcId: "unknown",
8157
+ result: {
8158
+ ok: false,
8159
+ error: {
8160
+ code: "bad-request",
8161
+ message: "body too large",
8162
+ details: {}
8163
+ }
8164
+ }
8165
+ }));
8166
+ });
8167
+ return;
8168
+ }
8169
+ readBody(req, 131072).then((body) => {
8170
+ let rpcId = "unknown";
8171
+ let path;
8172
+ try {
8173
+ const parsed = JSON.parse(body);
8174
+ if (typeof parsed.rpcId === "string") rpcId = parsed.rpcId;
8175
+ if (typeof parsed.payload?.path === "string") path = parsed.payload.path;
8176
+ } catch {}
8177
+ if (!workspaceAllowed(allowWorkspaces, path)) {
8178
+ res.writeHead(403, { "content-type": "application/json" });
8179
+ res.end(workspaceDenyResponse(rpcId));
8180
+ return;
8181
+ }
8182
+ proxy(req, res, body);
8183
+ }).catch(() => {
8184
+ res.writeHead(400, { "content-type": "application/json" });
8185
+ res.end(JSON.stringify({
8186
+ type: "server-response",
8187
+ rpcId: "unknown",
8188
+ result: {
8189
+ ok: false,
8190
+ error: {
8191
+ code: "bad-request",
8192
+ message: "body too large",
8193
+ details: {}
8194
+ }
8195
+ }
8196
+ }));
8197
+ });
8198
+ return;
8199
+ }
8200
+ if (req.method === "PUT" && url.pathname === "/serenity/config") {
8201
+ if (!originAllowed(req.headers.origin, req.headers.host ?? "", mainPort)) {
8202
+ console.warn(`[serenity-hooks] 拒绝跨源配置写(Origin=${String(req.headers.origin)}):session=${session.user}`);
8203
+ res.writeHead(403, { "content-type": "application/json" });
8204
+ res.end(JSON.stringify({ error: "cross-origin config write rejected" }));
8205
+ return;
8206
+ }
8207
+ }
7563
8208
  proxy(req, res);
7564
8209
  return;
7565
8210
  }
7566
8211
  if (req.method === "POST" && url.pathname === "/serenity/login") {
7567
8212
  let body = "";
8213
+ req.on("error", () => {
8214
+ try {
8215
+ res.destroy();
8216
+ } catch {}
8217
+ });
8218
+ res.on("error", () => {
8219
+ try {
8220
+ req.destroy();
8221
+ } catch {}
8222
+ });
7568
8223
  req.on("data", (chunk) => {
7569
8224
  body += chunk.toString("utf-8");
7570
8225
  });
@@ -7572,75 +8227,169 @@ function startGateway(config, getAccounts) {
7572
8227
  const params = new URLSearchParams(body);
7573
8228
  const user = params.get("user") ?? "";
7574
8229
  const password = params.get("password") ?? "";
7575
- if (verifyGatewayLogin(getAccounts(), user, password)) {
7576
- const token = issueToken();
8230
+ const code = params.get("code") ?? "";
8231
+ const remote = req.socket.remoteAddress ?? "unknown";
8232
+ const cookieCsrf = cookieValue(req.headers.cookie, "serenity_csrf");
8233
+ const formCsrf = csrfFromRequest(req, params);
8234
+ if (cookieCsrf === void 0 || formCsrf === null || !safeEqual(cookieCsrf, formCsrf)) {
8235
+ console.warn(`[serenity-hooks] 登录拒绝(CSRF 校验失败):user=${user} ip=${remote}`);
8236
+ res.writeHead(403, { "content-type": "text/html; charset=utf-8" });
8237
+ res.end(loginPageHtml("会话校验失败,请刷新页面重试"));
8238
+ return;
8239
+ }
8240
+ const lockedRemaining = accountLockRemaining(user);
8241
+ if (lockedRemaining > 0) {
8242
+ console.warn(`[serenity-hooks] 登录拒绝(账号锁定中):user=${user} ip=${remote} 剩余=${Math.ceil(lockedRemaining / 6e4)}min`);
8243
+ res.writeHead(429, {
8244
+ "content-type": "text/html; charset=utf-8",
8245
+ "retry-after": String(Math.ceil(lockedRemaining / 1e3))
8246
+ });
8247
+ res.end(loginPageHtml(`账号已锁定,请 ${Math.ceil(lockedRemaining / 6e4)} 分钟后再试`));
8248
+ return;
8249
+ }
8250
+ const accounts = getAccounts();
8251
+ const account = accounts.find((a) => a.user === user);
8252
+ const passOk = account !== void 0 && verifyGatewayLogin(accounts, user, password);
8253
+ let totpOk = true;
8254
+ if (totpEnabled && passOk && account !== void 0 && typeof account.totpSecret === "string" && account.totpSecret !== "") {
8255
+ const hit = verifyTotpCode(account.totpSecret, code);
8256
+ totpOk = hit !== null;
8257
+ if (totpOk && hit !== null && lastTotpCounter.get(account.id) === hit) totpOk = false;
8258
+ if (totpOk && hit !== null) lastTotpCounter.set(account.id, hit);
8259
+ }
8260
+ if (passOk && totpOk) {
8261
+ resetFailState(user);
8262
+ const token = issueToken(user);
7577
8263
  res.writeHead(302, {
7578
8264
  location: "/",
7579
- "set-cookie": `${cookieName}=${token}; HttpOnly; SameSite=Strict; Path=/`
8265
+ "set-cookie": `${cookieName}=${token}; HttpOnly; SameSite=Strict; Path=/${cookieSecure ? "; Secure" : ""}`
7580
8266
  });
7581
8267
  res.end();
8268
+ console.log(`[serenity-hooks] 登录成功: user=${user} ip=${remote}`);
7582
8269
  return;
7583
8270
  }
8271
+ const lockMs = recordLoginFailure(user);
8272
+ console.warn(`[serenity-hooks] 登录失败: user=${user} ip=${remote} reason=${!passOk ? "bad-password" : "bad-totp"}${lockMs > 0 ? ` 锁定=${Math.ceil(lockMs / 6e4)}min` : ""}`);
8273
+ const msg = lockMs > 0 ? `尝试过多,账号已锁定 ${Math.ceil(lockMs / 6e4)} 分钟` : passOk ? "验证码错误" : "用户名或密码错误";
7584
8274
  res.writeHead(401, { "content-type": "text/html; charset=utf-8" });
7585
- res.end(loginPageHtml("用户名或密码错误"));
8275
+ res.end(loginPageHtml(msg));
7586
8276
  });
7587
8277
  return;
7588
8278
  }
7589
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
7590
- res.end(loginPageHtml(""));
8279
+ const csrf = newCsrfToken();
8280
+ res.writeHead(200, {
8281
+ "content-type": "text/html; charset=utf-8",
8282
+ "set-cookie": `serenity_csrf=${csrf}; HttpOnly; SameSite=Strict; Path=/${cookieSecure ? "; Secure" : ""}`
8283
+ });
8284
+ res.end(loginPageHtml("", csrf));
7591
8285
  });
7592
8286
  server.on("upgrade", (req, socket, head) => {
7593
8287
  if (!authed(req)) {
7594
8288
  socket.destroy();
7595
8289
  return;
7596
8290
  }
8291
+ socket.on("error", () => {
8292
+ try {
8293
+ usocket?.destroy();
8294
+ } catch {}
8295
+ });
8296
+ let usocket;
7597
8297
  const upstream = request({
7598
8298
  host: "127.0.0.1",
7599
8299
  port: mainPort,
7600
8300
  path: req.url ?? "/",
7601
8301
  method: req.method,
7602
- headers: {
7603
- ...req.headers,
7604
- host: `127.0.0.1:${mainPort}`
8302
+ headers: buildProxyHeaders(req.headers, mainPort)
8303
+ });
8304
+ upstream.on("upgrade", (ures, usock, uhead) => {
8305
+ usocket = usock;
8306
+ usock.on("error", () => {
8307
+ try {
8308
+ socket.destroy();
8309
+ } catch {}
8310
+ });
8311
+ const statusLine = `HTTP/1.1 ${ures.statusCode ?? 101} ${ures.statusMessage ?? "Switching Protocols"}\r\n`;
8312
+ const headerLines = Object.entries(ures.headers).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : String(v)}\r\n`).join("");
8313
+ try {
8314
+ socket.write(statusLine + headerLines + "\r\n");
8315
+ } catch {
8316
+ usock.destroy();
8317
+ socket.destroy();
8318
+ return;
7605
8319
  }
8320
+ usock.pipe(socket);
8321
+ socket.pipe(usock);
8322
+ if (head.length > 0) usock.write(head);
8323
+ if (uhead.length > 0) socket.write(uhead);
7606
8324
  });
7607
- upstream.on("upgrade", (_ures, usocket, uhead) => {
7608
- usocket.pipe(socket);
7609
- socket.pipe(usocket);
7610
- if (uhead.length > 0) usocket.write(uhead);
7611
- if (head.length > 0) socket.write(head);
8325
+ upstream.on("response", (ures) => {
8326
+ const chunks = [];
8327
+ ures.on("data", (c) => chunks.push(c));
8328
+ ures.on("end", () => {
8329
+ const body = Buffer.concat(chunks);
8330
+ const statusLine = `HTTP/1.1 ${ures.statusCode ?? 502} ${ures.statusMessage ?? ""}\r\n`;
8331
+ const headerLines = Object.entries(ures.headers).filter(([k]) => ![
8332
+ "transfer-encoding",
8333
+ "connection",
8334
+ "upgrade"
8335
+ ].includes(k.toLowerCase())).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : String(v)}\r\n`).join("");
8336
+ try {
8337
+ socket.write(statusLine + headerLines + `content-length: ${body.length}\r\n\r\n`);
8338
+ socket.write(body);
8339
+ socket.end();
8340
+ } catch {
8341
+ socket.destroy();
8342
+ }
8343
+ });
8344
+ ures.on("error", () => socket.destroy());
7612
8345
  });
7613
8346
  upstream.on("error", () => socket.destroy());
7614
8347
  upstream.end();
7615
8348
  });
7616
- server.listen(port, host);
8349
+ const listenWithRetry = (attempt = 0) => {
8350
+ const onError = (err) => {
8351
+ if (err.code === "EADDRINUSE" && attempt < 10) {
8352
+ console.warn(`[serenity-hooks] gateway 端口 ${port} 被占用(${err.code}),1s 后重试(${attempt + 1}/10)…`);
8353
+ setTimeout(() => listenWithRetry(attempt + 1), 1e3);
8354
+ return;
8355
+ }
8356
+ console.warn(`[serenity-hooks] gateway 监听 ${host}:${port} 失败: ${err.message ?? String(err)}`);
8357
+ };
8358
+ server.removeAllListeners("error");
8359
+ server.on("error", onError);
8360
+ server.removeAllListeners("clientError");
8361
+ server.on("clientError", (_err, socket) => {
8362
+ try {
8363
+ socket.destroy();
8364
+ } catch {}
8365
+ });
8366
+ server.listen(port, host);
8367
+ };
8368
+ listenWithRetry();
7617
8369
  return {
7618
8370
  server,
7619
8371
  dispose: () => {
7620
- tokens.clear();
7621
8372
  server.close();
7622
8373
  }
7623
8374
  };
7624
8375
  }
7625
8376
  /** 注册 gateway(index.ts apply 调用)。
7626
- * root 发现:dsh web 进程 cwd 是 $HOME(非 CCC)——不能靠 process.cwd()。
7627
- * 改为:agent/session-start 的 header.cwd 解析 CCC 根(首个会话出现即发现)。
7628
- * 幂等 sync:仅当 配置/端口/root 变化时重建监听器。
7629
- * 事件驱动:config-updated(/serenity/config PUT 后 emit)触发重新 sync。
8377
+ * 归属原则(v1.22):gateway 是 plugin 全局能力——enabled 开关读 DSH settings
8378
+ * (readSimpleSettings().gatewayEnabled),host/port/accounts 读 plugin 全局文件
8379
+ * (readAdvancedSettings(),~/.dsh/serenity-hooks.json)。**不依赖任何具体 CCC**。
8380
+ * 幂等 sync:仅当 配置/端口/开关 变化时重建监听器。
8381
+ * 事件驱动:config-updated(/serenity/config PUT 后 emit)+ DSH settings 变化(onChange emit)触发重新 sync。
7630
8382
  */
7631
- function registerGateway(ctx, opts) {
8383
+ function registerGateway(ctx) {
7632
8384
  let current = null;
7633
- let discoveredRoot = null;
7634
8385
  let lastSig = null;
7635
- const resolveRoot = opts.resolveRoot ?? ((cwd) => findSerenityRoot(cwd));
7636
8386
  const sync = () => {
7637
8387
  try {
7638
- const root = discoveredRoot;
7639
8388
  const webServer = ctx.get?.("webServer");
7640
- if (!root || !webServer?.port) return;
7641
- const settings = readAdvancedSettings(root);
7642
- const enabled = settings.gateway.enabled;
7643
- const sig = `${enabled}|${settings.gateway.host}|${settings.gateway.port}|${settings.gateway.accounts.length}|${webServer.port}`;
8389
+ if (!webServer?.port) return;
8390
+ const enabled = readSimpleSettings().gatewayEnabled;
8391
+ const settings = readAdvancedSettings();
8392
+ const sig = `${enabled}|${settings.gateway.host}|${settings.gateway.port}|${settings.gateway.accounts.length}|${settings.gateway.workspaces.join(",")}|${webServer.port}`;
7644
8393
  if (sig === lastSig) return;
7645
8394
  lastSig = sig;
7646
8395
  if (current) {
@@ -7648,7 +8397,7 @@ function registerGateway(ctx, opts) {
7648
8397
  current = null;
7649
8398
  }
7650
8399
  if (!enabled) {
7651
- console.log("[serenity-hooks] gateway 已停止(enabled=false)");
8400
+ console.log("[serenity-hooks] gateway 已停止(gatewayEnabled=false,可在 dsh 设置面板开启)");
7652
8401
  return;
7653
8402
  }
7654
8403
  current = startGateway({
@@ -7656,31 +8405,37 @@ function registerGateway(ctx, opts) {
7656
8405
  port: settings.gateway.port,
7657
8406
  cookieName: "serenity_session",
7658
8407
  mainPort: webServer.port,
7659
- loginDelayMs: 0
7660
- }, () => readAdvancedSettings(root).gateway.accounts);
8408
+ loginDelayMs: 0,
8409
+ allowWorkspaces: settings.gateway.workspaces,
8410
+ cookieSecure: settings.gateway.cookieSecure === true,
8411
+ allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate !== false,
8412
+ totpEnabled: settings.gateway.totpEnabled === true
8413
+ }, () => readAdvancedSettings().gateway.accounts);
7661
8414
  const accounts = settings.gateway.accounts.length;
7662
- console.log(`[serenity-hooks] gateway 已启动: http://${settings.gateway.host}:${settings.gateway.port} → 127.0.0.1:${webServer.port}` + (accounts === 0 ? "(⚠️ 未配置账号,登录页将提示)" : `(${accounts} 个账号)`));
8415
+ const wsNote = settings.gateway.workspaces.length === 0 ? "" : `;工作区白名单 ${settings.gateway.workspaces.length} 条`;
8416
+ console.log(`[serenity-hooks] gateway 已启动: http://${settings.gateway.host}:${settings.gateway.port} → 127.0.0.1:${webServer.port}` + (accounts === 0 ? "(⚠️ 未配置账号,登录页将提示)" : `(${accounts} 个账号)`) + wsNote);
7663
8417
  } catch (err) {
7664
8418
  console.warn(`[serenity-hooks] gateway 同步失败: ${String(err?.message ?? err)}`);
7665
8419
  }
7666
8420
  };
8421
+ sync();
7667
8422
  ctx.on("agent/session-start", (payload) => {
7668
8423
  try {
7669
8424
  const cwd = payload.agent?.session?.header?.cwd;
7670
- if (!cwd) return;
7671
- const root = resolveRoot(cwd);
7672
- if (!root) return;
7673
- if (discoveredRoot !== root) {
7674
- discoveredRoot = root;
7675
- lastSig = null;
8425
+ if (cwd) {
8426
+ const root = findSerenityRoot(cwd);
8427
+ if (root && migrateLegacyLocalstore(root)) console.log(`[serenity-hooks] ✓ 已迁移 serenityAdvanced(CCC localstore → plugin 全局文件)`);
7676
8428
  }
7677
- sync();
7678
8429
  } catch {}
8430
+ sync();
7679
8431
  });
7680
8432
  ctx.on("serenity/config-updated", () => {
7681
8433
  lastSig = null;
7682
8434
  sync();
7683
8435
  });
8436
+ ctx.on("serenity/settings-changed", () => {
8437
+ sync();
8438
+ });
7684
8439
  }
7685
8440
  //#endregion
7686
8441
  //#region src/index.ts
@@ -7694,7 +8449,8 @@ const inject = [
7694
8449
  "skills",
7695
8450
  "agentLoop",
7696
8451
  "agents",
7697
- "systemPrompt"
8452
+ "systemPrompt",
8453
+ "sessionProjections"
7698
8454
  ];
7699
8455
  const Config = z.object({
7700
8456
  serenityConfigPaths: z.array(z.string()).default([...DEFAULT_SERENITY_CONFIG_PATHS]),
@@ -7745,7 +8501,8 @@ function apply(ctx, config) {
7745
8501
  });
7746
8502
  if (config.api) registerStatusApi(ctx, { configPaths: config.serenityConfigPaths });
7747
8503
  registerSettingsSection(ctx, config);
7748
- registerGateway(ctx, {});
8504
+ registerGateway(ctx);
8505
+ registerRebuildTurnHook(ctx);
7749
8506
  if (config.env) registerEnv(ctx);
7750
8507
  if (config.opencodeSkills) registerOpencodeSkills(ctx);
7751
8508
  registerBootstrap(ctx);