@shgroup/dsh-serenity-hooks 1.23.0 → 1.23.2

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
@@ -5417,11 +5417,18 @@ function namingTitleFor(active) {
5417
5417
  }
5418
5418
  /**
5419
5419
  * use 激活宁静号会话后,把当前 dsh 会话重命名为命名标题(`S###-日期`)。
5420
- * 门控:naming.enabled + sessionTitle 服务存在;失败不静默——返回 null 且输出
5421
- * 失败原因(调用方 console.warn),保证可观测性(v1.22.9)。
5422
- * @returns { title, ok } 或 { ok:false, reason }(未执行/失败均返回对象,非 null 歧义)
5420
+ * 门控:naming.enabled + sessionTitle 服务存在;失败不静默——返回结果对象
5421
+ * 而非 null(v1.22.9),调用方决定可见性。
5422
+ *
5423
+ * v1.23.2 修复(this 绑定):第三参从**解构的裸 rename 函数**改为**整个
5424
+ * sessionTitle 服务对象**——内部以 `titles.rename(session, title)` **方法调用**
5425
+ * (this = titles 服务实例)。旧实现调用点 `const rename = titles.rename` 解构
5426
+ * 后传入,方法内部读 `this.assertServiceActive` → this=undefined 抛错
5427
+ * (日志实证:`Cannot read properties of undefined (reading 'assertServiceActive')`;
5428
+ * 与 v1.20.2/1.20.3 图片落盘同款解构丢 this bug)。
5429
+ * @returns { title, ok } 或 { ok:false, reason }(未执行/失败均返回对象)
5423
5430
  */
5424
- function renameDshSessionOnUse(deps, session, rename, active) {
5431
+ function renameDshSessionOnUse(deps, session, titles, active) {
5425
5432
  if (!deps.namingEnabled) return {
5426
5433
  ok: false,
5427
5434
  reason: "naming.enabled=false"
@@ -5430,9 +5437,13 @@ function renameDshSessionOnUse(deps, session, rename, active) {
5430
5437
  ok: false,
5431
5438
  reason: "sessionTitle service unavailable"
5432
5439
  };
5440
+ if (!titles || typeof titles.rename !== "function") return {
5441
+ ok: false,
5442
+ reason: "sessionTitle service unavailable"
5443
+ };
5433
5444
  const title = namingTitleFor(active);
5434
5445
  try {
5435
- rename(session, title);
5446
+ titles.rename(session, title);
5436
5447
  return {
5437
5448
  ok: true,
5438
5449
  title
@@ -5622,16 +5633,16 @@ function createSessionTool(ctx) {
5622
5633
  try {
5623
5634
  const info = getActiveSessionInfo(scope);
5624
5635
  if (info) {
5625
- const rename = (ctx.get?.("sessionTitle"))?.rename;
5636
+ const titles = ctx.get?.("sessionTitle");
5626
5637
  const dshSession = exec.agent?.session;
5627
- if (dshSession && typeof rename === "function") {
5638
+ if (dshSession) {
5628
5639
  const result = renameDshSessionOnUse({
5629
5640
  namingEnabled: readSimpleSettings().namingEnabled,
5630
5641
  sessionTitleAvailable: true
5631
- }, dshSession, rename, info);
5642
+ }, dshSession, titles, info);
5632
5643
  if (result.ok) console.log(`[serenity-hooks] dsh 会话已重命名: ${String(dshSession.id ?? "?")} → ${result.title}`);
5633
5644
  else console.warn(`[serenity-hooks] dsh 会话重命名未执行: ${result.reason}`);
5634
- } else console.warn(`[serenity-hooks] dsh 会话重命名未执行: sessionTitle 服务不可用或缺少 agent session`);
5645
+ } else console.warn(`[serenity-hooks] dsh 会话重命名未执行: 缺少 agent session`);
5635
5646
  }
5636
5647
  } catch (err) {
5637
5648
  console.warn(`[serenity-hooks] dsh 会话重命名异常: ${String(err?.message ?? err)}`);
@@ -6409,204 +6420,527 @@ function truncateContent(content, maxChars) {
6409
6420
  if (content.length <= maxChars) return content;
6410
6421
  return content.slice(0, maxChars) + `\n... (truncated, original length ${content.length} chars)`;
6411
6422
  }
6412
- //#endregion
6413
- //#region src/seams/system-prompt.ts
6414
- /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及) */
6415
- const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/g;
6416
- function sanitizeSkillContent(content) {
6417
- return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
6423
+ const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
6424
+ /** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
6425
+ function base32Decode(input) {
6426
+ const clean = input.toUpperCase().replace(/[\s=]/g, "");
6427
+ if (clean.length === 0) throw new Error("empty base32");
6428
+ const out = [];
6429
+ let bits = 0;
6430
+ let value = 0;
6431
+ for (const ch of clean) {
6432
+ const idx = B32_ALPHABET.indexOf(ch);
6433
+ if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
6434
+ value = value << 5 | idx;
6435
+ bits += 5;
6436
+ if (bits >= 8) {
6437
+ out.push(value >>> bits - 8 & 255);
6438
+ bits -= 8;
6439
+ }
6440
+ }
6441
+ return Uint8Array.from(out);
6418
6442
  }
6419
- /** 1) ACC 块:身份 + CCC 名/Root + 内置工具清单(工具名换本插件真实 11 工具) */
6420
- function accBlock(root) {
6421
- const cccName = basename(root);
6422
- return [
6423
- "",
6424
- "=== Serenity ACC ===",
6425
- `ACC: dsh-serenity-hooks v${ACC_VERSION}`,
6426
- `CCC: ${cccName}`,
6427
- "",
6428
- "You are running inside a Concrete Cognitive Container (CCC) —",
6429
- "the runtime instance of an Abstract Cognitive Container (ACC).",
6430
- "The ACC (this plugin) provides the following built-in tools:",
6431
- "",
6432
- " cc_fs — CCC filesystem operations (root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
6433
- " session — session lifecycle (list/show/create/health/qa/archive/summary)",
6434
- " acc_kit — ACC utility kit (health: CCC three principles / time: now / wait: wait N seconds)",
6435
- " cc_git — git operations (status/commit/push/log)",
6436
- " acc_msm — MSM framework (list/exec/register/deregister/check/guide)",
6437
- " eap — return the full EAP cognitive quality framework",
6438
- " neat — return the full Neat design collaboration protocol",
6439
- " cce — return the full Cognitive Continuity Engineering framework",
6440
- " loop — run a dedicated model-specific agent in repeated rounds toward a goal",
6441
- " session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
6442
- " localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
6443
- "",
6444
- " ℹ️ 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",
6445
- "",
6446
- "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.",
6447
- "",
6448
- "Additional MSMs registered by this CCC are available — call acc_msm list to discover them.",
6449
- ""
6450
- ].join("\n");
6443
+ /** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
6444
+ function totpCode(secretBase32, counter) {
6445
+ const key = base32Decode(secretBase32);
6446
+ const msg = Buffer.alloc(8);
6447
+ msg.writeBigUInt64BE(BigInt(counter));
6448
+ const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
6449
+ const offset = digest[digest.length - 1] & 15;
6450
+ const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
6451
+ return String(bin % 10 ** 6).padStart(6, "0");
6451
6452
  }
6452
- /** 2) CCE 块:逐字对齐 osp(CCE 5 行为约束 + H_op 操作熵) */
6453
- function cceBlock() {
6454
- return [
6455
- "",
6456
- "=== Serenity CCE ===",
6457
- "",
6458
- "You are operating inside a Cognitive Container governed by Cognitive Continuity",
6459
- "Engineering (CCE) — the engineering discipline of maintaining identity, accessibility,",
6460
- "and evolution of a cognitive entity through time under bounded resources.",
6461
- "",
6462
- "CCE does not optimize cognition. It preserves the conditions under which cognition",
6463
- "can continue.",
6464
- "",
6465
- "FIVE BEHAVIORAL CONSTRAINTS (engineering requirements, not suggestions):",
6466
- "",
6467
- "1. Continuity — every interaction modifies the container's future state. Before",
6468
- " acting, consult what came before — prior decisions, abstractions, constraints.",
6469
- " You are part of a trajectory, not a fresh start.",
6470
- "",
6471
- "2. Bounded Space — the container has boundaries. Respect them. Do not assume",
6472
- " knowledge that has not been accumulated within this container.",
6473
- "",
6474
- "3. Entropy is Intrinsic — every cognitive system accumulates entropy (duplication,",
6475
- " obsolescence, conflict, fragmentation, drift). When you produce output, consider",
6476
- " whether you are adding entropy or reducing it. Favor entropy-reducing actions —",
6477
- " organizing, deduplicating, cross-referencing, abstracting.",
6478
- "",
6479
- "4. Reconstruction > Preservation — stored artifacts have value only insofar as",
6480
- " they enable future cognition to recover the reasoning that produced them. When",
6481
- " recording decisions, ensure reconstruction is possible — not just conclusions,",
6482
- " but rationale, alternatives considered, and constraints that shaped the choice.",
6483
- "",
6484
- "5. Multi-Agent Cognition — the container is shared. Continuity belongs to the",
6485
- " container, not to any individual agent. Write for future agents who will enter",
6486
- " after you leave. They should be able to pick up where you left off.",
6487
- "",
6488
- "OPERATIONAL ENTROPY: The container's health metric is operational cognitive entropy",
6489
- "(H_op) — the excess cognitive cost for agents to complete tasks due to disorder.",
6490
- "The container is healthy when H_op ≤ H_critical (agents can still function). The",
6491
- "continuity condition: organization must at minimum match accumulation (ΔH_org ≥ ΔH_in).",
6492
- "Your actions affect H_op — unorganized output increases it, organization decreases it.",
6493
- "",
6494
- "THIS IS PERSISTENCE ENGINEERING: The goal is not to become greater. The goal is to",
6495
- "remain coherent. CCE has no terminal KPI — continuity is maintained while the entity",
6496
- "exists, not optimized toward an endpoint.",
6497
- ""
6498
- ].join("\n");
6453
+ /** 当前 epoch */
6454
+ function nowEpochSeconds() {
6455
+ return Math.floor(Date.now() / 1e3);
6499
6456
  }
6500
6457
  /**
6501
- * Principles 块(v1.19.8 合并,S142):认知容器本体论(why)+ 操作边界(operational
6502
- * boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
6503
- * 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
6504
- * (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
6458
+ * 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
6459
+ * 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
6460
+ * @returns 命中的 counter(用于防重放);不匹配返回 null。
6505
6461
  */
6506
- function principlesBlock(root) {
6507
- return [
6508
- "",
6509
- "=== Serenity Principles ===",
6510
- "Why a cognitive container: all work is cognition — every artifact, decision,",
6511
- "and line of code is a product of thought; and from cognition, any work can",
6512
- "be built. In this frame, the world contains no errors — only insufficient",
6513
- "cognition. A setback is a gap to be filled (read, ask, research), not a",
6514
- "fault to be hidden. Never disguise or excuse what you do not know;",
6515
- "not-knowing is a state to be repaired, and reporting it is the first repair.",
6516
- "",
6517
- "The session-trajectory relation: a session is the rebuildable carrier of a",
6518
- "trajectory. SESSION.md is the trajectory's persistent body — it never moves;",
6519
- "the current conversation is a temporary work copy that may be discarded and",
6520
- "rebuilt (session_rebuild). Identity belongs to the trajectory, not to any",
6521
- "session.",
6522
- "",
6523
- "MSM principles — machinery before improvisation:",
6524
- "- Determinism first: use a registered Mech before hand-rolling; reserve",
6525
- " Semi-Mech for genuine judgment points.",
6526
- "- Single source of truth: an MSM is the only decoder of its own usage",
6527
- " (--help/--schema); documents must not duplicate it.",
6528
- "- Registered to act: no tool exists unless it is on the manifest.",
6529
- "",
6530
- "Operational boundaries:",
6531
- `Root: ${root}`,
6532
- " • File access — read/edit/write/grep/glob are confined to Root; paths outside Root are rejected (RR5)",
6533
- " • Shell — use acc_msm by default. Note: bash may be disabled",
6534
- " • Subagent — copies ALL parent constraints: file boundary, shell rules, session rules (no bypass)",
6535
- " • Session-first — before starting multi-step work, propose an existing or new AGENT_SESSIONS entry; wait for user \"use\" or \"使用\" to confirm",
6536
- ""
6537
- ].join("\n");
6462
+ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
6463
+ if (!/^\d{6}$/.test(code)) return null;
6464
+ const current = Math.floor(nowSeconds / 30);
6465
+ for (let offset = -1; offset <= 1; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
6466
+ return null;
6538
6467
  }
6468
+ //#endregion
6469
+ //#region src/config-ops.ts
6539
6470
  /**
6540
- * EAP 自检提示块(DSH 扩展,无 osp 对应——osp 无此块)。
6541
- * 每次输出前的机械自检清单,强化 EAP 表现(E↑ 显式/R↓ 可重建/S↑ 稳定)。
6542
- * 独立块而非塞进 CCE/Constraints:后两者受 osp-alignment 逐字节断言约束。
6471
+ * config-ops.ts plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
6472
+ *
6473
+ * 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
6474
+ * 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
6475
+ * CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
6476
+ * `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
6477
+ * 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
6478
+ *
6479
+ * 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
6480
+ * env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
6481
+ * 权限:0600(含账号密码 hash,敏感)。
6482
+ *
6483
+ * 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
6484
+ * (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
6543
6485
  */
6544
- function eapBlock() {
6545
- return [
6546
- "",
6547
- "=== Serenity EAP ===",
6548
- "Self-check before every output (Explicit Abstraction Principle: the functional",
6549
- "value of a thought equals its external reconstructability):",
6550
- " • E↑ Explicit — variables/entities clearly defined, relationships with",
6551
- " direction/cardinality, boundaries drawn; avoid ambiguous words (\"handle\",",
6552
- " \"optimize\" → be specific)",
6553
- " • R↓ Reconstructable — key decisions record rationale and alternatives;",
6554
- " no level-skipping (align the upper layer before descending)",
6555
- " • S↑ Stable — structures regenerate repeatably, no reliance on implicit",
6556
- " context",
6557
- ""
6558
- ].join("\n");
6486
+ /** 高级设定节名(localstore.json 顶层) */
6487
+ const ADVANCED_SECTION = "serenityAdvanced";
6488
+ /** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
6489
+ function defaultAdvancedSettings() {
6490
+ return {
6491
+ gateway: {
6492
+ enabled: false,
6493
+ host: "0.0.0.0",
6494
+ port: 3081,
6495
+ accounts: [],
6496
+ workspaces: [],
6497
+ cookieSecure: false,
6498
+ allowWorkspaceCreate: true,
6499
+ totpEnabled: false
6500
+ },
6501
+ rebuild: {
6502
+ enabled: true,
6503
+ thresholdRatio: .9
6504
+ },
6505
+ naming: { enabled: true },
6506
+ persona: {
6507
+ mode: "",
6508
+ overrideText: ""
6509
+ }
6510
+ };
6559
6511
  }
6560
- /**
6561
- * Metaphor 强化块(v1.19.6,S142):宁静号宇宙隐喻域(船/海/罗盘/日志/船员)。
6562
- * 隐喻是记忆钩子——形象唤起约束,前述块保持规则精确。每条隐喻 = 一个不可违背的
6563
- * 物理事实 + 行为判据(verdict)。全英文(与 CCE/Constraints 一致);独立块可
6564
- * 回退;无 osp 对应(dsp 扩展,不进对齐断言参照)。
6565
- */
6566
- function metaphorBlock() {
6567
- return [
6568
- "",
6569
- "=== Serenity Metaphor ===",
6570
- "The Serenity Universe one ship, one sea. Metaphors are memory hooks:",
6571
- "they make constraints vivid, while the rules above stay precise. Each",
6572
- "metaphor is an unbreakable physical fact; violating one is a behavioral",
6573
- "violation. The universe is structured in three layers — the Ship (the",
6574
- "container itself), the Voyage (the cognitive lifecycle), the Crew",
6575
- "(multi-agent collaboration); every metaphor maps to one protocol",
6576
- "constraint. The Sea has no mistakes only waters you have not yet charted.",
6577
- "",
6578
- "THE SHIP the container itself",
6579
- "",
6580
- "1. The Hull Bounded Space. You exist only inside this ship. Cargo",
6581
- " outside the hull (knowledge the container has not accumulated) does",
6582
- " not exist; do not assume it. Verdict: citing facts absent from the",
6583
- " container = overload.",
6584
- "",
6585
- "2. Deck Order → Entropy (H_op). Clutter on deck raises the cost of",
6586
- " finding things. H_op ≤ H_critical = the ship stays afloat.",
6587
- " Verdict: disorganized output = stones on deck.",
6588
- "",
6589
- "3. Engineering Drawings → EAP. Every part dimensioned (E↑), the",
6590
- " drawings rebuild the whole machine (R↓), the drawings are reusable",
6591
- " (S↑). Verdict: an undimensioned part = unassemblable.",
6592
- "",
6593
- "4. The Machinery → MSM (Mech & Semi-Mech). The ship's equipment is",
6594
- " machinery: registered, deterministic, self-describing. Turn the",
6595
- " crank of a Mech and the action is exact; the wheel with a helmsman",
6596
- " (Semi-Mech) steers where judgment is needed. Verdict: hand-rolling",
6597
- " what a machine already does = wasting the crew.",
6598
- "",
6599
- "5. The Manifest → Single Source of Truth. Every tool exists only if it",
6600
- " is on the manifest (mech-registry); there is exactly one manifest.",
6601
- " An MSM self-describes (--help/--schema) — the manifest is the only",
6602
- " key. Verdict: duplicating a tool's usage in documents = two",
6603
- " contradictory charts.",
6604
- "",
6605
- "THE VOYAGE the cognitive lifecycle",
6606
- "",
6607
- "6. Harbor Inspection → First Anchor. The first anchor = departure",
6608
- " inspection: confirm identity (ACC manifesto), logbook (SESSION),",
6609
- " ballast (constraints) before setting sail. Verdict: skipping the",
6512
+ const SCRYPT_KEYLEN = 32;
6513
+ /** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
6514
+ function hashPassword(password) {
6515
+ const salt = randomBytes(16);
6516
+ const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
6517
+ return `${salt.toString("hex")}:${derived.toString("hex")}`;
6518
+ }
6519
+ /** 校验密码与存储 hash(timing-safe) */
6520
+ function verifyPassword(password, stored) {
6521
+ const idx = stored.indexOf(":");
6522
+ if (idx <= 0) return false;
6523
+ const salt = Buffer.from(stored.slice(0, idx), "hex");
6524
+ const expected = Buffer.from(stored.slice(idx + 1), "hex");
6525
+ const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
6526
+ return expected.length === derived.length && timingSafeEqual(expected, derived);
6527
+ }
6528
+ /** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME ~/.dsh */
6529
+ function globalConfigPath() {
6530
+ const override = process.env.SERENITY_HOOKS_CONFIG;
6531
+ if (override && override !== "") return override;
6532
+ const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
6533
+ return join(dshHome, "serenity-hooks.json");
6534
+ }
6535
+ function readFileSafe(p) {
6536
+ if (!existsSync(p)) return {};
6537
+ try {
6538
+ const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
6539
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
6540
+ return {};
6541
+ } catch {
6542
+ return {};
6543
+ }
6544
+ }
6545
+ function writeFileSafe(p, data) {
6546
+ writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
6547
+ try {
6548
+ chmodSync(p, 384);
6549
+ } catch {}
6550
+ }
6551
+ /** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
6552
+ function mergeWithDefaults(raw) {
6553
+ const def = defaultAdvancedSettings();
6554
+ if (raw === null || typeof raw !== "object") return def;
6555
+ const o = raw;
6556
+ const gateway = o.gateway ?? {};
6557
+ const rebuild = o.rebuild ?? {};
6558
+ const naming = o.naming ?? {};
6559
+ const persona = o.persona ?? {};
6560
+ return {
6561
+ gateway: {
6562
+ enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
6563
+ host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
6564
+ port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
6565
+ 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,
6566
+ workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
6567
+ cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
6568
+ allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
6569
+ totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
6570
+ },
6571
+ rebuild: {
6572
+ enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
6573
+ thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
6574
+ },
6575
+ naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled },
6576
+ persona: {
6577
+ mode: typeof persona.mode === "string" ? persona.mode : def.persona.mode,
6578
+ overrideText: typeof persona.overrideText === "string" ? persona.overrideText : def.persona.overrideText
6579
+ }
6580
+ };
6581
+ }
6582
+ /** 读取全局配置(文件缺失/坏 JSON → 默认值) */
6583
+ function readAdvancedSettings() {
6584
+ return mergeWithDefaults(readFileSafe(globalConfigPath()));
6585
+ }
6586
+ /** 写入全局配置(整体替换) */
6587
+ function writeAdvancedSettings(settings) {
6588
+ writeFileSafe(globalConfigPath(), settings);
6589
+ }
6590
+ /**
6591
+ * 部分更新:传入 Partial,深合并到现有值。
6592
+ * accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
6593
+ */
6594
+ function updateAdvancedSettings(patch) {
6595
+ const current = readAdvancedSettings();
6596
+ const gw = patch.gateway;
6597
+ const rb = patch.rebuild;
6598
+ const nm = patch.naming;
6599
+ const ps = patch.persona;
6600
+ const next = {
6601
+ gateway: gw !== void 0 ? {
6602
+ enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
6603
+ host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
6604
+ port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
6605
+ accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
6606
+ workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
6607
+ cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
6608
+ allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
6609
+ totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
6610
+ } : current.gateway,
6611
+ rebuild: rb !== void 0 ? {
6612
+ enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
6613
+ thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
6614
+ } : current.rebuild,
6615
+ naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming,
6616
+ persona: ps !== void 0 ? {
6617
+ mode: typeof ps.mode === "string" ? ps.mode : current.persona.mode,
6618
+ overrideText: typeof ps.overrideText === "string" ? ps.overrideText : current.persona.overrideText
6619
+ } : current.persona
6620
+ };
6621
+ writeAdvancedSettings(next);
6622
+ return next;
6623
+ }
6624
+ /**
6625
+ * 一次性迁移(v1.21.x → v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
6626
+ * 新版归 plugin 全局文件。全局文件已存在 → 跳过(幂等);localstore 无旧节 → 跳过。
6627
+ * @param root - CCC 根(localstore.json 所在目录)
6628
+ * @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
6629
+ */
6630
+ function migrateLegacyLocalstore(root) {
6631
+ if (!root) return false;
6632
+ const gpath = globalConfigPath();
6633
+ if (existsSync(gpath)) return false;
6634
+ const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
6635
+ if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
6636
+ writeFileSafe(gpath, legacy);
6637
+ return true;
6638
+ }
6639
+ /** 持久化 → wire(剥离 passHash/totpSecret;只留布尔) */
6640
+ function toWire(settings) {
6641
+ return {
6642
+ gateway: {
6643
+ enabled: settings.gateway.enabled,
6644
+ host: settings.gateway.host,
6645
+ port: settings.gateway.port,
6646
+ accounts: settings.gateway.accounts.map((a) => ({
6647
+ id: a.id,
6648
+ user: a.user,
6649
+ hasPassword: a.passHash !== "",
6650
+ hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
6651
+ })),
6652
+ workspaces: [...settings.gateway.workspaces],
6653
+ cookieSecure: settings.gateway.cookieSecure,
6654
+ allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
6655
+ totpEnabled: settings.gateway.totpEnabled
6656
+ },
6657
+ rebuild: settings.rebuild,
6658
+ naming: settings.naming,
6659
+ persona: settings.persona
6660
+ };
6661
+ }
6662
+ /**
6663
+ * wire → 持久化(面板 PUT 用):
6664
+ * accounts 元素可选带 `pass`:非空 → 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
6665
+ * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
6666
+ */
6667
+ function applyWirePatch(wire) {
6668
+ const current = readAdvancedSettings();
6669
+ const patch = {};
6670
+ if (wire.gateway !== void 0) {
6671
+ const gwPatch = { ...current.gateway };
6672
+ if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
6673
+ if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
6674
+ if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
6675
+ if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
6676
+ if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
6677
+ if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
6678
+ if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
6679
+ if (Array.isArray(wire.gateway.accounts)) {
6680
+ const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
6681
+ gwPatch.accounts = wire.gateway.accounts.map((a) => {
6682
+ const existing = byId.get(a.id);
6683
+ const pass = a.pass;
6684
+ const totp = a.totpSecret;
6685
+ const totpReset = a.totpReset === true;
6686
+ const totpConfirm = a.totpConfirm;
6687
+ if (typeof totp === "string" && totp !== "") {
6688
+ 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`);
6689
+ }
6690
+ const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
6691
+ const withTotp = {
6692
+ id: a.id,
6693
+ user: a.user,
6694
+ ...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
6695
+ };
6696
+ if (typeof pass === "string" && pass !== "") return {
6697
+ ...withTotp,
6698
+ passHash: hashPassword(pass)
6699
+ };
6700
+ if (existing) return {
6701
+ ...withTotp,
6702
+ passHash: existing.passHash
6703
+ };
6704
+ throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
6705
+ });
6706
+ }
6707
+ patch.gateway = gwPatch;
6708
+ }
6709
+ if (wire.rebuild !== void 0) {
6710
+ const rbPatch = { ...current.rebuild };
6711
+ if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
6712
+ if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
6713
+ patch.rebuild = rbPatch;
6714
+ }
6715
+ if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
6716
+ if (wire.persona !== void 0) {
6717
+ const psPatch = { ...current.persona };
6718
+ if (typeof wire.persona.mode === "string") psPatch.mode = wire.persona.mode;
6719
+ if (typeof wire.persona.overrideText === "string") psPatch.overrideText = wire.persona.overrideText;
6720
+ patch.persona = psPatch;
6721
+ }
6722
+ return updateAdvancedSettings(patch);
6723
+ }
6724
+ //#endregion
6725
+ //#region src/seams/system-prompt.ts
6726
+ /** 读取 persona 彩蛋配置(plugin 全局;未配置 → mode='' 彩蛋关闭) */
6727
+ function readPersonaSettings() {
6728
+ try {
6729
+ const s = readAdvancedSettings();
6730
+ return {
6731
+ mode: s.persona.mode ?? "",
6732
+ overrideText: s.persona.overrideText ?? ""
6733
+ };
6734
+ } catch {
6735
+ return {
6736
+ mode: "",
6737
+ overrideText: ""
6738
+ };
6739
+ }
6740
+ }
6741
+ /** 过滤掉对 agent 隐藏的内容(safe-mode 是用户能力,不对 agent 提及) */
6742
+ const HIDDEN_LINES = /安全模式|safe-mode|\.serenity-safe-on/g;
6743
+ function sanitizeSkillContent(content) {
6744
+ return content.split("\n").filter((line) => !HIDDEN_LINES.test(line)).join("\n");
6745
+ }
6746
+ /** 1) ACC 块:身份 + CCC 名/Root + 内置工具清单(工具名换本插件真实 11 工具) */
6747
+ function accBlock(root) {
6748
+ const cccName = basename(root);
6749
+ return [
6750
+ "",
6751
+ "=== Serenity ACC ===",
6752
+ `ACC: dsh-serenity-hooks v${ACC_VERSION}`,
6753
+ `CCC: ${cccName}`,
6754
+ "",
6755
+ "You are running inside a Concrete Cognitive Container (CCC) —",
6756
+ "the runtime instance of an Abstract Cognitive Container (ACC).",
6757
+ "The ACC (this plugin) provides the following built-in tools:",
6758
+ "",
6759
+ " cc_fs — CCC filesystem operations (root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
6760
+ " session — session lifecycle (list/show/create/health/qa/archive/summary)",
6761
+ " acc_kit — ACC utility kit (health: CCC three principles / time: now / wait: wait N seconds)",
6762
+ " cc_git — git operations (status/commit/push/log)",
6763
+ " acc_msm — MSM framework (list/exec/register/deregister/check/guide)",
6764
+ " eap — return the full EAP cognitive quality framework",
6765
+ " neat — return the full Neat design collaboration protocol",
6766
+ " cce — return the full Cognitive Continuity Engineering framework",
6767
+ " loop — run a dedicated model-specific agent in repeated rounds toward a goal",
6768
+ " session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
6769
+ " localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
6770
+ "",
6771
+ " ℹ️ 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",
6772
+ "",
6773
+ "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.",
6774
+ "",
6775
+ "Additional MSMs registered by this CCC are available — call acc_msm list to discover them.",
6776
+ ""
6777
+ ].join("\n");
6778
+ }
6779
+ /** 2) CCE 块:逐字对齐 osp(CCE 5 行为约束 + H_op 操作熵) */
6780
+ function cceBlock() {
6781
+ return [
6782
+ "",
6783
+ "=== Serenity CCE ===",
6784
+ "",
6785
+ "You are operating inside a Cognitive Container governed by Cognitive Continuity",
6786
+ "Engineering (CCE) — the engineering discipline of maintaining identity, accessibility,",
6787
+ "and evolution of a cognitive entity through time under bounded resources.",
6788
+ "",
6789
+ "CCE does not optimize cognition. It preserves the conditions under which cognition",
6790
+ "can continue.",
6791
+ "",
6792
+ "FIVE BEHAVIORAL CONSTRAINTS (engineering requirements, not suggestions):",
6793
+ "",
6794
+ "1. Continuity — every interaction modifies the container's future state. Before",
6795
+ " acting, consult what came before — prior decisions, abstractions, constraints.",
6796
+ " You are part of a trajectory, not a fresh start.",
6797
+ "",
6798
+ "2. Bounded Space — the container has boundaries. Respect them. Do not assume",
6799
+ " knowledge that has not been accumulated within this container.",
6800
+ "",
6801
+ "3. Entropy is Intrinsic — every cognitive system accumulates entropy (duplication,",
6802
+ " obsolescence, conflict, fragmentation, drift). When you produce output, consider",
6803
+ " whether you are adding entropy or reducing it. Favor entropy-reducing actions —",
6804
+ " organizing, deduplicating, cross-referencing, abstracting.",
6805
+ "",
6806
+ "4. Reconstruction > Preservation — stored artifacts have value only insofar as",
6807
+ " they enable future cognition to recover the reasoning that produced them. When",
6808
+ " recording decisions, ensure reconstruction is possible — not just conclusions,",
6809
+ " but rationale, alternatives considered, and constraints that shaped the choice.",
6810
+ "",
6811
+ "5. Multi-Agent Cognition — the container is shared. Continuity belongs to the",
6812
+ " container, not to any individual agent. Write for future agents who will enter",
6813
+ " after you leave. They should be able to pick up where you left off.",
6814
+ "",
6815
+ "OPERATIONAL ENTROPY: The container's health metric is operational cognitive entropy",
6816
+ "(H_op) — the excess cognitive cost for agents to complete tasks due to disorder.",
6817
+ "The container is healthy when H_op ≤ H_critical (agents can still function). The",
6818
+ "continuity condition: organization must at minimum match accumulation (ΔH_org ≥ ΔH_in).",
6819
+ "Your actions affect H_op — unorganized output increases it, organization decreases it.",
6820
+ "",
6821
+ "THIS IS PERSISTENCE ENGINEERING: The goal is not to become greater. The goal is to",
6822
+ "remain coherent. CCE has no terminal KPI — continuity is maintained while the entity",
6823
+ "exists, not optimized toward an endpoint.",
6824
+ ""
6825
+ ].join("\n");
6826
+ }
6827
+ /**
6828
+ * Principles 块(v1.19.8 合并,S142):认知容器本体论(why)+ 操作边界(operational
6829
+ * boundaries)。原独立 Principles 与 Constraints 合并——同属容器约束体系,先原则
6830
+ * 后边界(从抽象到具体,重建视角 R↓)。**注意:Constraints 不再作为独立对齐块存在
6831
+ * (spec 修订:同步 osp compacting.ts——Constraints 内容并入本块,工具名仍为平台真实名)。**
6832
+ *
6833
+ * v1.23.1 persona:`omitMsmPrinciples=true` 时剥离 MSM 原则段(彩蛋替换面)——
6834
+ * 用户 persona 文本承接"指令遵循风格",本体论/关系段/操作边界(安全硬约束)永远保留。
6835
+ */
6836
+ function principlesBlock(root, omitMsmPrinciples = false) {
6837
+ const lines = [
6838
+ "",
6839
+ "=== Serenity Principles ===",
6840
+ "Why a cognitive container: all work is cognition — every artifact, decision,",
6841
+ "and line of code is a product of thought; and from cognition, any work can",
6842
+ "be built. In this frame, the world contains no errors — only insufficient",
6843
+ "cognition. A setback is a gap to be filled (read, ask, research), not a",
6844
+ "fault to be hidden. Never disguise or excuse what you do not know;",
6845
+ "not-knowing is a state to be repaired, and reporting it is the first repair.",
6846
+ "",
6847
+ "The session-trajectory relation: a session is the rebuildable carrier of a",
6848
+ "trajectory. SESSION.md is the trajectory's persistent body — it never moves;",
6849
+ "the current conversation is a temporary work copy that may be discarded and",
6850
+ "rebuilt (session_rebuild). Identity belongs to the trajectory, not to any",
6851
+ "session.",
6852
+ ""
6853
+ ];
6854
+ 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.", "");
6855
+ 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", "");
6856
+ return lines.join("\n");
6857
+ }
6858
+ /**
6859
+ * 彩蛋 persona 块(v1.23.1,S142 用户需求):
6860
+ * 用户配置的替换文本替代 EAP 块 + MSM 原则段(输出约束/指令遵循约束)。
6861
+ * 独立标记头 `=== Serenity Persona ===`(幂等检测兼容);装配位置 = EAP 原位。
6862
+ * mode 空 = 彩蛋关闭 → 返回空串(装配层回退默认 EAP + 完整 Principles)。
6863
+ */
6864
+ function personaBlock(mode, overrideText) {
6865
+ if (mode === "" || overrideText.trim() === "") return "";
6866
+ return [
6867
+ "",
6868
+ "=== Serenity Persona ===",
6869
+ overrideText.trimEnd(),
6870
+ ""
6871
+ ].join("\n");
6872
+ }
6873
+ /**
6874
+ * EAP 自检提示块(DSH 扩展,无 osp 对应——osp 无此块)。
6875
+ * 每次输出前的机械自检清单,强化 EAP 表现(E↑ 显式/R↓ 可重建/S↑ 稳定)。
6876
+ * 独立块而非塞进 CCE/Constraints:后两者受 osp-alignment 逐字节断言约束。
6877
+ */
6878
+ function eapBlock() {
6879
+ return [
6880
+ "",
6881
+ "=== Serenity EAP ===",
6882
+ "Self-check before every output (Explicit Abstraction Principle: the functional",
6883
+ "value of a thought equals its external reconstructability):",
6884
+ " • E↑ Explicit — variables/entities clearly defined, relationships with",
6885
+ " direction/cardinality, boundaries drawn; avoid ambiguous words (\"handle\",",
6886
+ " \"optimize\" → be specific)",
6887
+ " • R↓ Reconstructable — key decisions record rationale and alternatives;",
6888
+ " no level-skipping (align the upper layer before descending)",
6889
+ " • S↑ Stable — structures regenerate repeatably, no reliance on implicit",
6890
+ " context",
6891
+ ""
6892
+ ].join("\n");
6893
+ }
6894
+ /**
6895
+ * Metaphor 强化块(v1.19.6,S142):宁静号宇宙隐喻域(船/海/罗盘/日志/船员)。
6896
+ * 隐喻是记忆钩子——形象唤起约束,前述块保持规则精确。每条隐喻 = 一个不可违背的
6897
+ * 物理事实 + 行为判据(verdict)。全英文(与 CCE/Constraints 一致);独立块可
6898
+ * 回退;无 osp 对应(dsp 扩展,不进对齐断言参照)。
6899
+ */
6900
+ function metaphorBlock() {
6901
+ return [
6902
+ "",
6903
+ "=== Serenity Metaphor ===",
6904
+ "The Serenity Universe — one ship, one sea. Metaphors are memory hooks:",
6905
+ "they make constraints vivid, while the rules above stay precise. Each",
6906
+ "metaphor is an unbreakable physical fact; violating one is a behavioral",
6907
+ "violation. The universe is structured in three layers — the Ship (the",
6908
+ "container itself), the Voyage (the cognitive lifecycle), the Crew",
6909
+ "(multi-agent collaboration); every metaphor maps to one protocol",
6910
+ "constraint. The Sea has no mistakes — only waters you have not yet charted.",
6911
+ "",
6912
+ "THE SHIP — the container itself",
6913
+ "",
6914
+ "1. The Hull → Bounded Space. You exist only inside this ship. Cargo",
6915
+ " outside the hull (knowledge the container has not accumulated) does",
6916
+ " not exist; do not assume it. Verdict: citing facts absent from the",
6917
+ " container = overload.",
6918
+ "",
6919
+ "2. Deck Order → Entropy (H_op). Clutter on deck raises the cost of",
6920
+ " finding things. H_op ≤ H_critical = the ship stays afloat.",
6921
+ " Verdict: disorganized output = stones on deck.",
6922
+ "",
6923
+ "3. Engineering Drawings → EAP. Every part dimensioned (E↑), the",
6924
+ " drawings rebuild the whole machine (R↓), the drawings are reusable",
6925
+ " (S↑). Verdict: an undimensioned part = unassemblable.",
6926
+ "",
6927
+ "4. The Machinery → MSM (Mech & Semi-Mech). The ship's equipment is",
6928
+ " machinery: registered, deterministic, self-describing. Turn the",
6929
+ " crank of a Mech and the action is exact; the wheel with a helmsman",
6930
+ " (Semi-Mech) steers where judgment is needed. Verdict: hand-rolling",
6931
+ " what a machine already does = wasting the crew.",
6932
+ "",
6933
+ "5. The Manifest → Single Source of Truth. Every tool exists only if it",
6934
+ " is on the manifest (mech-registry); there is exactly one manifest.",
6935
+ " An MSM self-describes (--help/--schema) — the manifest is the only",
6936
+ " key. Verdict: duplicating a tool's usage in documents = two",
6937
+ " contradictory charts.",
6938
+ "",
6939
+ "THE VOYAGE — the cognitive lifecycle",
6940
+ "",
6941
+ "6. Harbor Inspection → First Anchor. The first anchor = departure",
6942
+ " inspection: confirm identity (ACC manifesto), logbook (SESSION),",
6943
+ " ballast (constraints) before setting sail. Verdict: skipping the",
6610
6944
  " anchor and working directly = sailing uninspected.",
6611
6945
  "",
6612
6946
  "7. The Logbook → Session Tracking. SESSION.md is the trajectory's logbook —",
@@ -6759,14 +7093,20 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
6759
7093
  * 身份(ACC)→ 世界模型(Metaphor)→ 信念/边界(Principles)→ 时间约束(CCE)
6760
7094
  * → 质量(EAP)→ 状态(SafeMode/Localstore)→ CCC 上下文(SKILL)→ 会话(Session)。
6761
7095
  * 认知展开顺序:我是谁 → 我所在的世界 → 为什么 → 如何一致 → 产物标准 → 当前状态 → 上下文。
7096
+ *
7097
+ * v1.23.1 persona(彩蛋):persona.mode 配置 → EAP 块替换为 Persona 块(输出约束),
7098
+ * Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
7099
+ * 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
6762
7100
  */
6763
7101
  function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
7102
+ const persona = readPersonaSettings();
7103
+ const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
6764
7104
  const parts = [
6765
7105
  accBlock(root),
6766
7106
  metaphorBlock(),
6767
- principlesBlock(root),
7107
+ principlesBlock(root, personaOn),
6768
7108
  cceBlock(),
6769
- eapBlock()
7109
+ personaOn ? personaBlock(persona.mode, persona.overrideText) : eapBlock()
6770
7110
  ];
6771
7111
  const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
6772
7112
  if (state) parts.push(state);
@@ -6888,417 +7228,137 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
6888
7228
  if (phase2) {
6889
7229
  lines.push("- ⚠️ **Phase 2 cognitive alignment interview pending**: work through the 5 Topics below and record answers in an AGENT_SESSIONS/ session");
6890
7230
  try {
6891
- const prompt = readFileSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"), "utf-8");
6892
- lines.push(truncateContent(prompt, Math.min(entrySkillMaxChars, 8e3)));
6893
- } catch {}
6894
- }
6895
- return lines.join("\n");
6896
- }
6897
- const PLUGIN_SOURCE = {
6898
- kind: "plugin",
6899
- plugin: "dsh-serenity-hooks"
6900
- };
6901
- /**
6902
- * ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + loop 模型 + Phase 2)。
6903
- * 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
6904
- * (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
6905
- * 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
6906
- */
6907
- function accMessage(root, configPaths, entrySkillMaxChars) {
6908
- const content = [{
6909
- type: "text",
6910
- text: accIdentityText(root, configPaths, entrySkillMaxChars)
6911
- }];
6912
- return createUserMessage({
6913
- content,
6914
- source: PLUGIN_SOURCE
6915
- });
6916
- }
6917
- /** 每 agent 是否已注入过(进程内存态) */
6918
- const injected = /* @__PURE__ */ new Set();
6919
- function agentKey(agent) {
6920
- return agent.session.id ?? "global";
6921
- }
6922
- function agentScope(agent) {
6923
- return agent.session.id ?? "default";
6924
- }
6925
- /**
6926
- * 重启恢复的根会话判定(S134 需求):
6927
- * 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
6928
- * - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
6929
- * - 派生会话:`parentSession` 存在(任何子会话)
6930
- * - loop 牛马:sessionId 固定 `loop-` 前缀(tools/loop.ts 生成)
6931
- * 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
6932
- */
6933
- function shouldAutoRestore(agent) {
6934
- const session = agent.session;
6935
- if (!session) return false;
6936
- if (session.header?.origin === "subagent") return false;
6937
- if (session.header?.parentSession) return false;
6938
- if (session.id?.startsWith("loop-")) return false;
6939
- return true;
6940
- }
6941
- /**
6942
- * 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
6943
- * 才自动恢复上次激活的宁静号会话——全新会话(新任务,如 apaas-26116)无历史 → 不恢复,
6944
- * 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
6945
- */
6946
- function shouldRestoreActive(agent) {
6947
- if (!shouldAutoRestore(agent)) return false;
6948
- const events = agent.session?.events;
6949
- return Array.isArray(events) && events.length > 0;
6950
- }
6951
- function registerContext(ctx, opts = {}) {
6952
- const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
6953
- const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
6954
- const seed = (agent) => {
6955
- const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
6956
- if (!root) return;
6957
- const key = agentKey(agent);
6958
- registerEntrySkillSection(agent, root);
6959
- const scope = agentScope(agent);
6960
- if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
6961
- if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
6962
- const info = parseSessionContextFromEvents(agent.session.events ?? []);
6963
- if (info) {
6964
- setActiveSessionInfo(scope, info);
6965
- console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${info.dirName}`);
6966
- }
6967
- }
6968
- } catch {}
6969
- if (injected.has(key)) return;
6970
- injected.add(key);
6971
- agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
6972
- syncSafeModeRestriction(agent, root);
6973
- };
6974
- if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
6975
- try {
6976
- seed(payload.agent);
6977
- } catch {}
6978
- });
6979
- if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
6980
- const { agent, messages } = payload;
6981
- const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
6982
- const key = agentKey(agent);
6983
- const downstream = await next();
6984
- if (root) {
6985
- try {
6986
- syncSafeModeRestriction(agent, root);
6987
- } catch {}
6988
- registerEntrySkillSection(agent, root);
6989
- }
6990
- if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
6991
- injected.add(key);
6992
- return {
6993
- kind: "enter",
6994
- messages: [
6995
- accMessage(root, configPaths, entrySkillMaxChars),
6996
- ...messages,
6997
- ...downstream.messages
6998
- ]
6999
- };
7000
- });
7001
- }
7002
- //#endregion
7003
- //#region src/seams/compact.ts
7004
- /**
7005
- * 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
7006
- * 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
7007
- */
7008
- function registerCompactRetention(ctx, opts = {}) {
7009
- const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
7010
- const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
7011
- ctx.on("session/event", (session, event) => {
7012
- if (event.type !== "compaction/end") return;
7013
- if (event.data.error) return;
7014
- const agent = ctx.agents.get(session.id);
7015
- if (!agent) return;
7016
- const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
7017
- if (!root) return;
7018
- try {
7019
- agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
7020
- } catch {}
7021
- });
7022
- }
7023
- const B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
7024
- /** base32(无填充,大小写不敏感)→ 字节;非法字符抛错。 */
7025
- function base32Decode(input) {
7026
- const clean = input.toUpperCase().replace(/[\s=]/g, "");
7027
- if (clean.length === 0) throw new Error("empty base32");
7028
- const out = [];
7029
- let bits = 0;
7030
- let value = 0;
7031
- for (const ch of clean) {
7032
- const idx = B32_ALPHABET.indexOf(ch);
7033
- if (idx === -1) throw new Error(`invalid base32 char: ${ch}`);
7034
- value = value << 5 | idx;
7035
- bits += 5;
7036
- if (bits >= 8) {
7037
- out.push(value >>> bits - 8 & 255);
7038
- bits -= 8;
7039
- }
7040
- }
7041
- return Uint8Array.from(out);
7042
- }
7043
- /** 一个时步的 TOTP code(6 位,前导零保留)。counter = floor(epochSeconds / step) */
7044
- function totpCode(secretBase32, counter) {
7045
- const key = base32Decode(secretBase32);
7046
- const msg = Buffer.alloc(8);
7047
- msg.writeBigUInt64BE(BigInt(counter));
7048
- const digest = createHmac("sha1", Buffer.from(key)).update(msg).digest();
7049
- const offset = digest[digest.length - 1] & 15;
7050
- const bin = (digest[offset] & 127) << 24 | digest[offset + 1] << 16 | digest[offset + 2] << 8 | digest[offset + 3];
7051
- return String(bin % 10 ** 6).padStart(6, "0");
7052
- }
7053
- /** 当前 epoch 秒 */
7054
- function nowEpochSeconds() {
7055
- return Math.floor(Date.now() / 1e3);
7056
- }
7057
- /**
7058
- * 校验用户输入的 code(允许 ±TOTP_WINDOW 时步漂移,防重放窗口内同 code 复用由
7059
- * 调用方按账号记录最近成功 counter 实现——本函数只做纯算法校验)。
7060
- * @returns 命中的 counter(用于防重放);不匹配返回 null。
7061
- */
7062
- function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
7063
- if (!/^\d{6}$/.test(code)) return null;
7064
- const current = Math.floor(nowSeconds / 30);
7065
- for (let offset = -1; offset <= 1; offset++) if (totpCode(secretBase32, current + offset) === code) return current + offset;
7066
- return null;
7067
- }
7068
- //#endregion
7069
- //#region src/config-ops.ts
7070
- /**
7071
- * config-ops.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
7072
- *
7073
- * 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
7074
- * 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
7075
- * CCC 的 localstore.json 只管 CCC 自己的凭据/配置。v1.21.x 曾把
7076
- * `serenityAdvanced` 存进 CCC localstore(归属错误 + 与 DSH settings 开关割裂),
7077
- * 本版本迁移到 plugin 全局文件(migrateLegacyLocalstore 一次性迁移)。
7078
- *
7079
- * 文件:$DSH_HOME/serenity-hooks.json(缺省 ~/.dsh/serenity-hooks.json;
7080
- * env SERENITY_HOOKS_CONFIG 可覆盖——测试/部署注入)。
7081
- * 权限:0600(含账号密码 hash,敏感)。
7082
- *
7083
- * 安全:密码仅存 scrypt hash(node:crypto 内置,零依赖);wire 层永不返回 hash
7084
- * (GET 只回 user/id,设置面板"密码"字段提交空串 = 不修改)。
7085
- */
7086
- /** 高级设定节名(localstore.json 顶层) */
7087
- const ADVANCED_SECTION = "serenityAdvanced";
7088
- /** 默认值(工厂——每次返回新对象,防止调用方意外共享引用) */
7089
- function defaultAdvancedSettings() {
7090
- return {
7091
- gateway: {
7092
- enabled: false,
7093
- host: "0.0.0.0",
7094
- port: 3081,
7095
- accounts: [],
7096
- workspaces: [],
7097
- cookieSecure: false,
7098
- allowWorkspaceCreate: true,
7099
- totpEnabled: false
7100
- },
7101
- rebuild: {
7102
- enabled: true,
7103
- thresholdRatio: .9
7104
- },
7105
- naming: { enabled: true }
7106
- };
7107
- }
7108
- const SCRYPT_KEYLEN = 32;
7109
- /** 生成 scrypt hash(格式 `salt:hex`);salt 16 字节随机 */
7110
- function hashPassword(password) {
7111
- const salt = randomBytes(16);
7112
- const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
7113
- return `${salt.toString("hex")}:${derived.toString("hex")}`;
7114
- }
7115
- /** 校验密码与存储 hash(timing-safe) */
7116
- function verifyPassword(password, stored) {
7117
- const idx = stored.indexOf(":");
7118
- if (idx <= 0) return false;
7119
- const salt = Buffer.from(stored.slice(0, idx), "hex");
7120
- const expected = Buffer.from(stored.slice(idx + 1), "hex");
7121
- const derived = scryptSync(password, salt, SCRYPT_KEYLEN);
7122
- return expected.length === derived.length && timingSafeEqual(expected, derived);
7123
- }
7124
- /** 全局配置文件路径:env SERENITY_HOOKS_CONFIG 覆盖(测试注入)→ $DSH_HOME → ~/.dsh */
7125
- function globalConfigPath() {
7126
- const override = process.env.SERENITY_HOOKS_CONFIG;
7127
- if (override && override !== "") return override;
7128
- const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
7129
- return join(dshHome, "serenity-hooks.json");
7130
- }
7131
- function readFileSafe(p) {
7132
- if (!existsSync(p)) return {};
7133
- try {
7134
- const v = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""));
7135
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
7136
- return {};
7137
- } catch {
7138
- return {};
7139
- }
7140
- }
7141
- function writeFileSafe(p, data) {
7142
- writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf-8");
7143
- try {
7144
- chmodSync(p, 384);
7145
- } catch {}
7231
+ const prompt = readFileSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"), "utf-8");
7232
+ lines.push(truncateContent(prompt, Math.min(entrySkillMaxChars, 8e3)));
7233
+ } catch {}
7234
+ }
7235
+ return lines.join("\n");
7146
7236
  }
7147
- /** 深合并默认值(缺省字段补齐;accounts 数组整体替换) */
7148
- function mergeWithDefaults(raw) {
7149
- const def = defaultAdvancedSettings();
7150
- if (raw === null || typeof raw !== "object") return def;
7151
- const o = raw;
7152
- const gateway = o.gateway ?? {};
7153
- const rebuild = o.rebuild ?? {};
7154
- const naming = o.naming ?? {};
7155
- return {
7156
- gateway: {
7157
- enabled: typeof gateway.enabled === "boolean" ? gateway.enabled : def.gateway.enabled,
7158
- host: typeof gateway.host === "string" && gateway.host !== "" ? gateway.host : def.gateway.host,
7159
- port: typeof gateway.port === "number" && Number.isInteger(gateway.port) ? gateway.port : def.gateway.port,
7160
- 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,
7161
- workspaces: Array.isArray(gateway.workspaces) ? gateway.workspaces.filter((w) => typeof w === "string" && w !== "") : def.gateway.workspaces,
7162
- cookieSecure: typeof gateway.cookieSecure === "boolean" ? gateway.cookieSecure : def.gateway.cookieSecure,
7163
- allowWorkspaceCreate: typeof gateway.allowWorkspaceCreate === "boolean" ? gateway.allowWorkspaceCreate : def.gateway.allowWorkspaceCreate,
7164
- totpEnabled: typeof gateway.totpEnabled === "boolean" ? gateway.totpEnabled : def.gateway.totpEnabled
7165
- },
7166
- rebuild: {
7167
- enabled: typeof rebuild.enabled === "boolean" ? rebuild.enabled : def.rebuild.enabled,
7168
- thresholdRatio: typeof rebuild.thresholdRatio === "number" ? rebuild.thresholdRatio : def.rebuild.thresholdRatio
7169
- },
7170
- naming: { enabled: typeof naming.enabled === "boolean" ? naming.enabled : def.naming.enabled }
7171
- };
7237
+ const PLUGIN_SOURCE = {
7238
+ kind: "plugin",
7239
+ plugin: "dsh-serenity-hooks"
7240
+ };
7241
+ /**
7242
+ * ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + loop 模型 + Phase 2)。
7243
+ * 完整身份(ACC 5 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
7244
+ * (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
7245
+ * 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
7246
+ */
7247
+ function accMessage(root, configPaths, entrySkillMaxChars) {
7248
+ const content = [{
7249
+ type: "text",
7250
+ text: accIdentityText(root, configPaths, entrySkillMaxChars)
7251
+ }];
7252
+ return createUserMessage({
7253
+ content,
7254
+ source: PLUGIN_SOURCE
7255
+ });
7172
7256
  }
7173
- /** 读取全局配置(文件缺失/坏 JSON 默认值) */
7174
- function readAdvancedSettings() {
7175
- return mergeWithDefaults(readFileSafe(globalConfigPath()));
7257
+ /** agent 是否已注入过(进程内存态) */
7258
+ const injected = /* @__PURE__ */ new Set();
7259
+ function agentKey(agent) {
7260
+ return agent.session.id ?? "global";
7176
7261
  }
7177
- /** 写入全局配置(整体替换) */
7178
- function writeAdvancedSettings(settings) {
7179
- writeFileSafe(globalConfigPath(), settings);
7262
+ function agentScope(agent) {
7263
+ return agent.session.id ?? "default";
7180
7264
  }
7181
7265
  /**
7182
- * 部分更新:传入 Partial,深合并到现有值。
7183
- * accounts 传入数组 → 整体替换;accounts 未传 → 保留现有。
7266
+ * 重启恢复的根会话判定(S134 需求):
7267
+ * 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
7268
+ * - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
7269
+ * - 派生会话:`parentSession` 存在(任何子会话)
7270
+ * - loop 牛马:sessionId 固定 `loop-` 前缀(tools/loop.ts 生成)
7271
+ * 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
7184
7272
  */
7185
- function updateAdvancedSettings(patch) {
7186
- const current = readAdvancedSettings();
7187
- const gw = patch.gateway;
7188
- const rb = patch.rebuild;
7189
- const nm = patch.naming;
7190
- const next = {
7191
- gateway: gw !== void 0 ? {
7192
- enabled: typeof gw.enabled === "boolean" ? gw.enabled : current.gateway.enabled,
7193
- host: typeof gw.host === "string" && gw.host !== "" ? gw.host : current.gateway.host,
7194
- port: typeof gw.port === "number" && Number.isInteger(gw.port) ? gw.port : current.gateway.port,
7195
- accounts: Array.isArray(gw.accounts) ? gw.accounts : current.gateway.accounts,
7196
- workspaces: Array.isArray(gw.workspaces) ? gw.workspaces.filter((w) => typeof w === "string" && w !== "") : current.gateway.workspaces,
7197
- cookieSecure: typeof gw.cookieSecure === "boolean" ? gw.cookieSecure : current.gateway.cookieSecure,
7198
- allowWorkspaceCreate: typeof gw.allowWorkspaceCreate === "boolean" ? gw.allowWorkspaceCreate : current.gateway.allowWorkspaceCreate,
7199
- totpEnabled: typeof gw.totpEnabled === "boolean" ? gw.totpEnabled : current.gateway.totpEnabled
7200
- } : current.gateway,
7201
- rebuild: rb !== void 0 ? {
7202
- enabled: typeof rb.enabled === "boolean" ? rb.enabled : current.rebuild.enabled,
7203
- thresholdRatio: typeof rb.thresholdRatio === "number" && rb.thresholdRatio > 0 && rb.thresholdRatio <= 1 ? rb.thresholdRatio : current.rebuild.thresholdRatio
7204
- } : current.rebuild,
7205
- naming: nm !== void 0 ? { enabled: typeof nm.enabled === "boolean" ? nm.enabled : current.naming.enabled } : current.naming
7206
- };
7207
- writeAdvancedSettings(next);
7208
- return next;
7273
+ function shouldAutoRestore(agent) {
7274
+ const session = agent.session;
7275
+ if (!session) return false;
7276
+ if (session.header?.origin === "subagent") return false;
7277
+ if (session.header?.parentSession) return false;
7278
+ if (session.id?.startsWith("loop-")) return false;
7279
+ return true;
7209
7280
  }
7210
7281
  /**
7211
- * 一次性迁移(v1.21.x v1.22):旧版把 `serenityAdvanced` 存在 CCC localstore.json;
7212
- * 新版归 plugin 全局文件。全局文件已存在 跳过(幂等);localstore 无旧节 → 跳过。
7213
- * @param root - CCC 根(localstore.json 所在目录)
7214
- * @returns true = 已迁移(旧节保留在 localstore 供回滚,读取方以全局文件为准)
7282
+ * 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
7283
+ * 才自动恢复上次激活的宁静号会话——全新会话(新任务,如 apaas-26116)无历史不恢复,
7284
+ * 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
7215
7285
  */
7216
- function migrateLegacyLocalstore(root) {
7217
- if (!root) return false;
7218
- const gpath = globalConfigPath();
7219
- if (existsSync(gpath)) return false;
7220
- const legacy = readFileSafe(join(root, "localstore.json"))[ADVANCED_SECTION];
7221
- if (legacy === void 0 || legacy === null || typeof legacy !== "object") return false;
7222
- writeFileSafe(gpath, legacy);
7223
- return true;
7286
+ function shouldRestoreActive(agent) {
7287
+ if (!shouldAutoRestore(agent)) return false;
7288
+ const events = agent.session?.events;
7289
+ return Array.isArray(events) && events.length > 0;
7224
7290
  }
7225
- /** 持久化 wire(剥离 passHash/totpSecret;只留布尔) */
7226
- function toWire(settings) {
7227
- return {
7228
- gateway: {
7229
- enabled: settings.gateway.enabled,
7230
- host: settings.gateway.host,
7231
- port: settings.gateway.port,
7232
- accounts: settings.gateway.accounts.map((a) => ({
7233
- id: a.id,
7234
- user: a.user,
7235
- hasPassword: a.passHash !== "",
7236
- hasTotp: typeof a.totpSecret === "string" && a.totpSecret !== ""
7237
- })),
7238
- workspaces: [...settings.gateway.workspaces],
7239
- cookieSecure: settings.gateway.cookieSecure,
7240
- allowWorkspaceCreate: settings.gateway.allowWorkspaceCreate,
7241
- totpEnabled: settings.gateway.totpEnabled
7242
- },
7243
- rebuild: settings.rebuild,
7244
- naming: settings.naming
7291
+ function registerContext(ctx, opts = {}) {
7292
+ const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
7293
+ const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
7294
+ const seed = (agent) => {
7295
+ const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
7296
+ if (!root) return;
7297
+ const key = agentKey(agent);
7298
+ registerEntrySkillSection(agent, root);
7299
+ const scope = agentScope(agent);
7300
+ if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
7301
+ if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
7302
+ const info = parseSessionContextFromEvents(agent.session.events ?? []);
7303
+ if (info) {
7304
+ setActiveSessionInfo(scope, info);
7305
+ console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${info.dirName}`);
7306
+ }
7307
+ }
7308
+ } catch {}
7309
+ if (injected.has(key)) return;
7310
+ injected.add(key);
7311
+ agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
7312
+ syncSafeModeRestriction(agent, root);
7245
7313
  };
7314
+ if (opts.seedOnStart ?? true) ctx.on("agent/session-start", (payload) => {
7315
+ try {
7316
+ seed(payload.agent);
7317
+ } catch {}
7318
+ });
7319
+ if (opts.injectOnPrompt ?? true) ctx.on("agent/pre-step", async (payload, next) => {
7320
+ const { agent, messages } = payload;
7321
+ const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
7322
+ const key = agentKey(agent);
7323
+ const downstream = await next();
7324
+ if (root) {
7325
+ try {
7326
+ syncSafeModeRestriction(agent, root);
7327
+ } catch {}
7328
+ registerEntrySkillSection(agent, root);
7329
+ }
7330
+ if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
7331
+ injected.add(key);
7332
+ return {
7333
+ kind: "enter",
7334
+ messages: [
7335
+ accMessage(root, configPaths, entrySkillMaxChars),
7336
+ ...messages,
7337
+ ...downstream.messages
7338
+ ]
7339
+ };
7340
+ });
7246
7341
  }
7342
+ //#endregion
7343
+ //#region src/seams/compact.ts
7247
7344
  /**
7248
- * wire 持久化(面板 PUT 用):
7249
- * accounts 元素可选带 `pass`:非空 重新 hash;空/缺省 → 保留现有 hash(按 id 匹配)。
7250
- * 新账号(id 不在现有)必须带非空 pass,否则抛错(无法生成 hash)。
7345
+ * 注册压缩保留:compact/end(成功)后重注入 ACC 身份。
7346
+ * 仅当 agent 工作目录在 CCC 内时生效(激活门控)。
7251
7347
  */
7252
- function applyWirePatch(wire) {
7253
- const current = readAdvancedSettings();
7254
- const patch = {};
7255
- if (wire.gateway !== void 0) {
7256
- const gwPatch = { ...current.gateway };
7257
- if (typeof wire.gateway.enabled === "boolean") gwPatch.enabled = wire.gateway.enabled;
7258
- if (typeof wire.gateway.host === "string" && wire.gateway.host !== "") gwPatch.host = wire.gateway.host;
7259
- if (typeof wire.gateway.port === "number" && Number.isInteger(wire.gateway.port)) gwPatch.port = wire.gateway.port;
7260
- if (Array.isArray(wire.gateway.workspaces)) gwPatch.workspaces = wire.gateway.workspaces.filter((w) => typeof w === "string" && w !== "");
7261
- if (typeof wire.gateway.cookieSecure === "boolean") gwPatch.cookieSecure = wire.gateway.cookieSecure;
7262
- if (typeof wire.gateway.allowWorkspaceCreate === "boolean") gwPatch.allowWorkspaceCreate = wire.gateway.allowWorkspaceCreate;
7263
- if (typeof wire.gateway.totpEnabled === "boolean") gwPatch.totpEnabled = wire.gateway.totpEnabled;
7264
- if (Array.isArray(wire.gateway.accounts)) {
7265
- const byId = new Map(current.gateway.accounts.map((a) => [a.id, a]));
7266
- gwPatch.accounts = wire.gateway.accounts.map((a) => {
7267
- const existing = byId.get(a.id);
7268
- const pass = a.pass;
7269
- const totp = a.totpSecret;
7270
- const totpReset = a.totpReset === true;
7271
- const totpConfirm = a.totpConfirm;
7272
- if (typeof totp === "string" && totp !== "") {
7273
- 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`);
7274
- }
7275
- const nextTotp = totpReset ? void 0 : typeof totp === "string" && totp !== "" ? totp : existing?.totpSecret;
7276
- const withTotp = {
7277
- id: a.id,
7278
- user: a.user,
7279
- ...nextTotp === void 0 ? {} : { totpSecret: nextTotp }
7280
- };
7281
- if (typeof pass === "string" && pass !== "") return {
7282
- ...withTotp,
7283
- passHash: hashPassword(pass)
7284
- };
7285
- if (existing) return {
7286
- ...withTotp,
7287
- passHash: existing.passHash
7288
- };
7289
- throw new Error(`Account "${a.user}" (id=${a.id}) has no password and no existing hash — new accounts must set a password`);
7290
- });
7291
- }
7292
- patch.gateway = gwPatch;
7293
- }
7294
- if (wire.rebuild !== void 0) {
7295
- const rbPatch = { ...current.rebuild };
7296
- if (typeof wire.rebuild.enabled === "boolean") rbPatch.enabled = wire.rebuild.enabled;
7297
- if (typeof wire.rebuild.thresholdRatio === "number" && wire.rebuild.thresholdRatio > 0 && wire.rebuild.thresholdRatio <= 1) rbPatch.thresholdRatio = wire.rebuild.thresholdRatio;
7298
- patch.rebuild = rbPatch;
7299
- }
7300
- if (wire.naming !== void 0 && typeof wire.naming.enabled === "boolean") patch.naming = { enabled: wire.naming.enabled };
7301
- return updateAdvancedSettings(patch);
7348
+ function registerCompactRetention(ctx, opts = {}) {
7349
+ const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
7350
+ const entrySkillMaxChars = opts.entrySkillMaxChars ?? 3e4;
7351
+ ctx.on("session/event", (session, event) => {
7352
+ if (event.type !== "compaction/end") return;
7353
+ if (event.data.error) return;
7354
+ const agent = ctx.agents.get(session.id);
7355
+ if (!agent) return;
7356
+ const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
7357
+ if (!root) return;
7358
+ try {
7359
+ agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
7360
+ } catch {}
7361
+ });
7302
7362
  }
7303
7363
  //#endregion
7304
7364
  //#region src/api.ts