@shgroup/dsh-serenity-hooks 1.70.0 → 1.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +21 -19
  2. package/dsh.plugin.json +2 -2
  3. package/lib/{ccc-C33QWoWZ.js → ccc-Dkoa2oW3.js} +16 -2
  4. package/lib/{ccc-roots-RzasspMr.js → ccc-roots-jZYR2GVE.js} +16 -10
  5. package/lib/client.js +2 -2
  6. package/lib/{gateway-auth.d.ts → gateway/auth.d.ts} +3 -3
  7. package/lib/{gateway-dsh-auth.d.ts → gateway/dsh-auth.d.ts} +1 -1
  8. package/lib/{gateway-proxy.d.ts → gateway/proxy.d.ts} +3 -3
  9. package/lib/{gateway.d.ts → gateway/server.d.ts} +5 -5
  10. package/lib/{totp.d.ts → gateway/totp.d.ts} +1 -1
  11. package/lib/{output-guard.d.ts → guard/filter.d.ts} +2 -2
  12. package/lib/{output-guard-seam.d.ts → guard/seam.d.ts} +2 -2
  13. package/lib/{weixin-api-BrRcPiQs.js → ilink-DEpmE3yi.js} +6 -6
  14. package/lib/im/accounts.d.ts +105 -0
  15. package/lib/{weixin-bridge.d.ts → im/bridge.d.ts} +16 -9
  16. package/lib/{im-bridge.d.ts → im/channels.d.ts} +3 -3
  17. package/lib/{weixin-send-endpoint.d.ts → im/endpoint.d.ts} +2 -2
  18. package/lib/{weixin-output-guard.d.ts → im/guard.d.ts} +1 -1
  19. package/lib/{weixin-hook.d.ts → im/hook.d.ts} +1 -1
  20. package/lib/{weixin-api.d.ts → im/ilink.d.ts} +3 -3
  21. package/lib/{human-channel.d.ts → im/inbound.d.ts} +4 -4
  22. package/lib/{weixin-route.d.ts → im/route.d.ts} +52 -11
  23. package/lib/{weixin-send-api.d.ts → im/send.d.ts} +3 -3
  24. package/lib/im/weixin-account.d.ts +45 -0
  25. package/lib/{im-weixin.d.ts → im/weixin-channel.d.ts} +2 -2
  26. package/lib/index.js +1008 -815
  27. package/lib/invariant.js +2 -2
  28. package/lib/{ccc-roots.d.ts → kernel/ccc-roots.d.ts} +10 -10
  29. package/lib/{ccc.d.ts → kernel/ccc.d.ts} +114 -44
  30. package/lib/{config-ops.d.ts → kernel/config.d.ts} +2 -2
  31. package/lib/kernel/constants.d.ts +20 -0
  32. package/lib/{invariant.d.ts → kernel/invariant.d.ts} +1 -1
  33. package/lib/{json.d.ts → kernel/json.d.ts} +1 -1
  34. package/lib/{message-source.d.ts → kernel/message-source.d.ts} +1 -1
  35. package/lib/{ports.d.ts → kernel/ports.d.ts} +1 -1
  36. package/lib/{time.d.ts → kernel/time.d.ts} +1 -1
  37. package/lib/message-source-kVoNK0C5.js +432 -0
  38. package/lib/{deepseek-vision-patch.d.ts → patches/deepseek-vision.d.ts} +3 -3
  39. package/lib/{opencode-provider.d.ts → patches/opencode-provider.d.ts} +1 -1
  40. package/lib/{web-fetch-provider.d.ts → patches/web-fetch.d.ts} +1 -1
  41. package/lib/{message-source-DhA1dVBi.js → profile-DyMJ_bfr.js} +277 -463
  42. package/lib/role/skiff/config.d.ts +79 -0
  43. package/lib/role/skiff/core.d.ts +155 -0
  44. package/lib/{skiff-debug.d.ts → role/skiff/debug.d.ts} +4 -4
  45. package/lib/role/skiff/profile.d.ts +115 -0
  46. package/lib/role/skiff/registry.d.ts +41 -0
  47. package/lib/{cheap-worker-ops.d.ts → role/worker/core.d.ts} +1 -1
  48. package/lib/{agent-idle.d.ts → role/worker/idle.d.ts} +2 -2
  49. package/lib/{cheap-worker-preset-inherit.d.ts → role/worker/preset.d.ts} +2 -2
  50. package/lib/{weixin-route-D_DvgPqy.js → route-f7JlinIt.js} +132 -47
  51. package/lib/seams/context.d.ts +3 -3
  52. package/lib/seams/guards.d.ts +18 -12
  53. package/lib/seams/keeper.d.ts +1 -1
  54. package/lib/seams/system-prompt.d.ts +3 -1
  55. package/lib/settings-lt1i3OZv.js +403 -0
  56. package/lib/{skills-discovery.d.ts → skills/discovery.d.ts} +1 -1
  57. package/lib/skills/resource-index.d.ts +2 -2
  58. package/lib/{face-host.d.ts → surface/faces.d.ts} +4 -4
  59. package/lib/{api.d.ts → surface/http.d.ts} +2 -2
  60. package/lib/{container-status.d.ts → surface/observation.d.ts} +16 -16
  61. package/lib/{settings-section.d.ts → surface/settings.d.ts} +1 -1
  62. package/lib/{status.d.ts → surface/status.d.ts} +2 -2
  63. package/lib/{usage-stats.d.ts → surface/usage.d.ts} +4 -4
  64. package/lib/{voyage-page.d.ts → surface/voyage/page.d.ts} +2 -2
  65. package/lib/{voyage-vocab.d.ts → surface/voyage/vocab.d.ts} +1 -1
  66. package/lib/tools/cognitive-trajectory.d.ts +2 -2
  67. package/lib/tools/{acc-diag.d.ts → diag/index.d.ts} +2 -2
  68. package/lib/{diag-ops.d.ts → tools/diag/service.d.ts} +7 -7
  69. package/lib/tools/eap.d.ts +1 -1
  70. package/lib/tools/fs/index.d.ts +7 -0
  71. package/lib/{fs-ops.d.ts → tools/fs/service.d.ts} +2 -2
  72. package/lib/tools/git/index.d.ts +4 -0
  73. package/lib/{git-ops.d.ts → tools/git/service.d.ts} +2 -2
  74. package/lib/tools/im-bridge.d.ts +2 -2
  75. package/lib/tools/{kit.d.ts → kit/index.d.ts} +1 -1
  76. package/lib/{kit-ops.d.ts → tools/kit/service.d.ts} +6 -6
  77. package/lib/tools/{localstore.d.ts → localstore/index.d.ts} +1 -1
  78. package/lib/{localstore-ops.d.ts → tools/localstore/service.d.ts} +2 -2
  79. package/lib/tools/{msm.d.ts → msm/index.d.ts} +1 -1
  80. package/lib/{msm-ops.d.ts → tools/msm/service.d.ts} +2 -2
  81. package/lib/tools/skiff-admin.d.ts +19 -14
  82. package/lib/{cognitive-trajectory-assistant.d.ts → trajectory/assistant.d.ts} +2 -2
  83. package/lib/{cognitive-trajectory-bound.d.ts → trajectory/bound.d.ts} +2 -2
  84. package/lib/{session-cleanup.d.ts → trajectory/cleanup.d.ts} +1 -1
  85. package/lib/{cognitive-trajectory-ops.d.ts → trajectory/core.d.ts} +55 -25
  86. package/lib/{live-sessions.d.ts → trajectory/live.d.ts} +1 -1
  87. package/lib/trajectory/prompt.d.ts +101 -0
  88. package/lib/{rebuild.d.ts → trajectory/rebuild.d.ts} +1 -1
  89. package/lib/{cognitive-trajectory-skills.d.ts → trajectory/skills.d.ts} +1 -1
  90. package/lib/{clock-runtime.d.ts → wake/clock.d.ts} +3 -3
  91. package/lib/{cro-guide.d.ts → wake/cro-guide.d.ts} +1 -1
  92. package/lib/{cro-log.d.ts → wake/cro-log.d.ts} +3 -3
  93. package/lib/{cro-turns.d.ts → wake/cro-turns.d.ts} +5 -5
  94. package/lib/{cro.d.ts → wake/cro.d.ts} +6 -6
  95. package/lib/{wake-registry.d.ts → wake/registry.d.ts} +2 -2
  96. package/lib/{wake-scheduler.d.ts → wake/scheduler.d.ts} +3 -3
  97. package/package.json +2 -2
  98. package/lib/constants.d.ts +0 -6
  99. package/lib/settings-section-0TPYWug9.js +0 -238
  100. package/lib/skiff-core.d.ts +0 -145
  101. package/lib/skiff-registry.d.ts +0 -26
  102. package/lib/skiff-role-cwxAt0gE.js +0 -203
  103. package/lib/skiff-role.d.ts +0 -89
  104. package/lib/tools/cc-fs.d.ts +0 -7
  105. package/lib/tools/git.d.ts +0 -4
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
- import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-RzasspMr.js";
3
- import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-C33QWoWZ.js";
4
- import { A as summarize, C as readTrajectoryUid, D as setActiveSessionInfo, E as sessionsRoot, F as localIdStamp, M as isoLocal, N as localFileStamp, O as setTrajectoryUid, P as localHuman, S as readActiveSessionMd, T as sessionEvents$1, _ as getActiveSessionInfo, a as pruneMissingBindings, b as namingTitleFor, c as setBindingStore, d as UID_FILENAME, f as archiveSession, g as findSession, h as extractSessionMdPathFromText, i as listBoundSessionIds, j as useSession, k as showSession, l as COGNITIVE_TRAJECTORY_ACTIONS, m as createSession, n as PLUGIN_SOURCE, o as readLastBound, p as clearActiveSessionInfo, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, u as DEFAULT_SESSION_SCOPE, v as listActiveTrajectories, w as resolveSessionByTitle, x as parseSessionContextFromEvents, y as listSessions } from "./message-source-DhA1dVBi.js";
5
- import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-D_DvgPqy.js";
6
- import { a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as skiffRoleFor$1, f as skiffSessionInfo$1, i as FACE_PORTS, l as WEIXIN_SEND_PORT, m as ACC_VERSION, n as readSimpleSettings, o as MAIN_WEB_PORT, p as unregisterSkiffSession$1, r as registerSettingsSection, s as MECHANISM_PORTS, u as registerSkiffSession$1 } from "./settings-section-0TPYWug9.js";
7
- import { a as isSkiffSessionId, c as resolveSkiffKind, f as systemPromptSource, i as createRolePromptReader, l as roleMsmWhitelist, n as buildSkiffBasePrompt, o as readSkiffRoles, r as cognitiveTrajectorySubset, s as resolveRoleSystemPrompt, u as roleToolWhitelist } from "./skiff-role-cwxAt0gE.js";
2
+ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-jZYR2GVE.js";
3
+ import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, g as readUtf8, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readCognitiveTrajectorySkills, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-Dkoa2oW3.js";
4
+ import { A as sessionEvents$1, C as listSessions, D as readActiveSessionMd, E as parseTrajectoryCode, F as useSession, I as isoLocal, L as localFileStamp, M as setActiveSessionInfo, N as showSession, O as readTrajectoryUid, P as summarize, R as localHuman, S as listActiveTrajectories, T as parseSessionContextFromEvents, _ as createSession, a as readSkiffProfileAt, b as getActiveSessionInfo, c as registerSkiffSession$1, d as unregisterSkiffSession$1, f as COGNITIVE_TRAJECTORY_ACTIONS, g as clearActiveSessionInfo, h as archiveSession, j as sessionsRoot, k as resolveSessionByTitle, l as skiffSessionInfo$1, m as UID_FILENAME, n as isSkiffSession, o as readSkiffProfileByCode, p as DEFAULT_SESSION_SCOPE, r as listSkiffProfiles, s as skiffProfileForSession, t as SKIFF_PROFILE_FILENAME, u as skiffTrajectoryDirNameFor, v as extractSessionMdPathFromText, w as namingTitleFor, y as findSession, z as localIdStamp } from "./profile-DyMJ_bfr.js";
5
+ import { a as readWeixinCredential, c as sanitizeFileName$1, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinInboundDir, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, t as extractWeixinMedia, u as weixinSessionIdForTrajectory } from "./route-f7JlinIt.js";
6
+ import { _ as resolveProfileSystemPrompt, a as GATEWAY_PORT, c as SKIFF_DEBUG_PORT, d as buildSkiffBasePrompt, f as createProfilePromptReader, g as profileToolWhitelist, h as profileSystemPromptSource, i as FACE_PORTS, l as WEIXIN_SEND_PORT, m as profileMsmWhitelist, n as readSimpleSettings, o as MAIN_WEB_PORT, p as profileCognitiveTrajectory, r as registerSettingsSection, s as MECHANISM_PORTS, u as SKIFF_PROFILE_HINT, v as ACC_VERSION, y as findPackageRoot } from "./settings-lt1i3OZv.js";
7
+ import { a as pruneMissingBindings, c as setBindingStore, i as listBoundSessionIds, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND } from "./message-source-kVoNK0C5.js";
8
8
  import { REGISTERED_TOOLS } from "./invariant.js";
9
- import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-BrRcPiQs.js";
9
+ import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-DEpmE3yi.js";
10
10
  import { createRequire } from "node:module";
11
11
  import z from "@deepseek-ai/schemastery";
12
12
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -25,9 +25,9 @@ import * as zlib from "node:zlib";
25
25
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
26
26
  import { lookup } from "node:dns/promises";
27
27
  import { isIP } from "node:net";
28
- //#region src/fs-ops.ts
28
+ //#region src/tools/fs/service.ts
29
29
  /**
30
- * fs-ops.ts — container_fs 纯操作层(cc_fs → container_fs,v1.30;零 DSH 依赖,可独立单测)
30
+ * tools/fs/service.ts — container_fs 纯操作层(cc_fs → container_fs,v1.30;零 DSH 依赖,可独立单测)
31
31
  *
32
32
  * 行为对齐 osp(opencode-serenity-plugin/src/fs/file-system-tool.ts)——osp 是 ACC 工具 spec:
33
33
  * - rm:目录需 recursive 才删除;非空目录无 recursive → [SKIP];保护 .serenity 与 CCC 根;dry-run 预览
@@ -416,12 +416,12 @@ function runCcFs(root, args) {
416
416
  }
417
417
  }
418
418
  //#endregion
419
- //#region src/tools/cc-fs.ts
419
+ //#region src/tools/fs/index.ts
420
420
  /**
421
- * cc-fs.ts — container_fs 真实 DSH 工具定义(defineTool)(v1.30:cc_fs → container_fs)
421
+ * tools/fs/index.ts — container_fs 真实 DSH 工具定义(defineTool)(v1.30:cc_fs → container_fs)
422
422
  *
423
423
  * 进程内注册(取代 v0.1 的 bash spawn runner):zod/schemastery 参数校验、
424
- * 规范 JSON 输出、纯 render 投影。逻辑在 fs-ops.ts(可单测)。
424
+ * 规范 JSON 输出、纯 render 投影。逻辑在 tools/fs/service.ts(可单测)。
425
425
  */
426
426
  function renderText$11(value) {
427
427
  return [{
@@ -504,7 +504,7 @@ const ccFsTool = defineTool({
504
504
  }
505
505
  });
506
506
  //#endregion
507
- //#region src/kit-ops.ts
507
+ //#region src/tools/kit/service.ts
508
508
  const KIT_ACTIONS = [
509
509
  "health",
510
510
  "time",
@@ -513,7 +513,7 @@ const KIT_ACTIONS = [
513
513
  async function runKit(root, args, hostCtx) {
514
514
  switch (args.action) {
515
515
  case "health": {
516
- const { containerStatus } = await Promise.resolve().then(() => container_status_exports);
516
+ const { containerStatus } = await Promise.resolve().then(() => observation_exports);
517
517
  const s = containerStatus({
518
518
  root,
519
519
  hostCtx
@@ -567,9 +567,9 @@ async function runKit(root, args, hostCtx) {
567
567
  }
568
568
  }
569
569
  //#endregion
570
- //#region src/tools/kit.ts
570
+ //#region src/tools/kit/index.ts
571
571
  /**
572
- * kit.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
572
+ * tools/kit/index.ts — dashboard 真实 DSH 工具定义(defineTool)(v1.30:acc_kit → dashboard)
573
573
  */
574
574
  function renderText$10(value) {
575
575
  return [{
@@ -607,9 +607,9 @@ function createKitTool(ctx) {
607
607
  });
608
608
  }
609
609
  //#endregion
610
- //#region src/git-ops.ts
610
+ //#region src/tools/git/service.ts
611
611
  /**
612
- * git-ops.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
612
+ * tools/git/service.ts — container_git 纯操作层(cc_git → container_git,v1.30;零 DSH 依赖)
613
613
  *
614
614
  * 行为对齐 osp(opencode-serenity-plugin/src/git/cc-git-tool.ts)——osp 是 ACC 工具 spec:
615
615
  * - status:JSON {clean, files:[{status,file}], summary, ahead, behind}
@@ -910,9 +910,9 @@ function runGit(root, args) {
910
910
  }
911
911
  }
912
912
  //#endregion
913
- //#region src/tools/git.ts
913
+ //#region src/tools/git/index.ts
914
914
  /**
915
- * git.ts — container_git 真实 DSH 工具定义(defineTool)(v1.30:cc_git → container_git)
915
+ * tools/git/index.ts — container_git 真实 DSH 工具定义(defineTool)(v1.30:cc_git → container_git)
916
916
  */
917
917
  function renderText$9(value) {
918
918
  return [{
@@ -971,9 +971,9 @@ const gitTool = defineTool({
971
971
  }
972
972
  });
973
973
  //#endregion
974
- //#region src/weixin-send-endpoint.ts
974
+ //#region src/im/endpoint.ts
975
975
  /**
976
- * weixin-send-endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
976
+ * im/endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
977
977
  *
978
978
  * 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
979
979
  * 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
@@ -981,7 +981,7 @@ const gitTool = defineTool({
981
981
  * 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
982
982
  * (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
983
983
  *
984
- * 同款先例:`host/effect.ts`(拆卸登记)、`agent-idle.ts`(等待结算)。
984
+ * 同款先例:`host/effect.ts`(拆卸登记)、`role/worker/idle.ts`(等待结算)。
985
985
  */
986
986
  let endpoint = null;
987
987
  /** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
@@ -993,9 +993,9 @@ function weixinSendEndpoint() {
993
993
  return endpoint;
994
994
  }
995
995
  //#endregion
996
- //#region src/msm-ops.ts
996
+ //#region src/tools/msm/service.ts
997
997
  /**
998
- * msm-ops.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
998
+ * tools/msm/service.ts — msm 纯操作层(acc_msm → msm 执行 + container_admin 管理面,v1.30;零 DSH 依赖)
999
999
  *
1000
1000
  * MSM(Mech & Semi-Mech)框架:exec 执行 / register|deregister|check 管理。
1001
1001
  * 复用 CCC 的 mech-registry.json(v1 或数组格式)。cwd 钉在 CCC 根。
@@ -1023,7 +1023,7 @@ function bunExecutablePath() {
1023
1023
  bunExeCache = null;
1024
1024
  return null;
1025
1025
  }
1026
- /** CCC 名:从 .serenity 解析(review P2-2 统一收口到 ccc.ts readCccName——跳 # 注释/空行首非空行;此处保留导出名兼容内部调用) */
1026
+ /** CCC 名:从 .serenity 解析(review P2-2 统一收口到 kernel/ccc.ts readCccName——跳 # 注释/空行首非空行;此处保留导出名兼容内部调用) */
1027
1027
  function readCccName(root) {
1028
1028
  return readCccName$1(root);
1029
1029
  }
@@ -1823,149 +1823,142 @@ async function runMsmAsync(root, args) {
1823
1823
  }
1824
1824
  }
1825
1825
  //#endregion
1826
- //#region src/cheap-worker-ops.ts
1826
+ //#region src/trajectory/prompt.ts
1827
1827
  /**
1828
- * cheap-worker-ops.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
1828
+ * trajectory/prompt.ts — **轨迹提示词**(Trajectory Prompt):一条轨迹"是谁、按什么章程工作"
1829
1829
  *
1830
- * 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
1831
- * 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
1832
- * 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
1830
+ * ## 概念(owner 2026-10-19 定向,逐字)
1831
+ *
1832
+ * > 「参考会话的系统提示词,我们建立**轨迹提示词**概念来代替人格。架构上会好很多」
1833
+ *
1834
+ * **轨迹提示词** = 一条认知轨迹的**可编辑章程声明**,由 ACC 按固定顺序装配进系统提示词。
1835
+ * 三个关键词各是一条判据:
1836
+ * - **可编辑**:住在 CCC(盘上),改它**不发版**(D23 归属判据的直接推论)
1837
+ * - **声明**:它声明**身份/章程/方法论**,**不声明能力**(能力面归 `skiff.json` 白名单与工具 schema)
1838
+ * - **固定顺序**:装配顺序由 ACC 定死(见 `seams/system-prompt.ts` 的 ⑧ 位)
1839
+ *
1840
+ * ## 为什么取代「人格」
1841
+ *
1842
+ * 「人格」把身份绑在"像不像一个人"上 ⇒ 因为叫人格,它被设计成**可以整体替换**
1843
+ * (`persona` 置换 EAP 块,即 review F-05/R-14 那个悬置冲突)。而身份与章程**不该被替换,
1844
+ * 只该被特化**。⇒ 本模块把"替换"改成"叠加":容器层与质量层永远保留,轨迹提示词只往上加。
1845
+ *
1846
+ * ## 落点(为什么在轨迹目录里)
1847
+ *
1848
+ * `AGENT_SESSIONS/<轨迹目录>/PROMPT.md`
1849
+ *
1850
+ * - **跟着轨迹走**:`archive` 时整个目录搬走,章程不会掉队(旧形态 `.opencode/skiff/<name>.md`
1851
+ * 与轨迹**分居两处** —— 多条轨迹共用一份文件正是那个后果)。
1852
+ * - **每条轨迹都能有**:不只是 skiff。普通轨迹同样需要"我是谁、按什么章程"。
1853
+ * - 与 `SESSION.md`(身体:延续/日志)、`skiff.json`(约束面:能力)三件并列,
1854
+ * 各管一件事,不合并(合并会让"改能力"与"改身份"互相牵连)。
1855
+ *
1856
+ * ## 兼容(不静默坏)
1857
+ *
1858
+ * `PROMPT.md` 不存在时**不报错**(普通轨迹常态)。skiff 的旧字段 `systemPromptFile`
1859
+ * 在兼容期仍被读取——优先级见 {@link resolveTrajectoryPrompt}。
1833
1860
  */
1834
- /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
1835
- function splitModel(model) {
1836
- const idx = model.indexOf("/");
1837
- if (idx < 0) return {
1838
- provider: void 0,
1839
- model
1840
- };
1841
- return {
1842
- provider: model.slice(0, idx),
1843
- model: model.slice(idx + 1)
1844
- };
1861
+ /** 轨迹提示词的固定文件名(**判据就是它存在**——不是目录名前缀,也不是配置里的一行) */
1862
+ const TRAJECTORY_PROMPT_FILENAME = "PROMPT.md";
1863
+ /** 轨迹目录的 PROMPT.md 绝对路径(不论存在与否——调用方自己判存在性) */
1864
+ function trajectoryPromptPath(root, dirName) {
1865
+ return join(sessionsRoot(root), dirName, TRAJECTORY_PROMPT_FILENAME);
1845
1866
  }
1846
- /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
1847
- function requireWhitelistedModel(model, models) {
1848
- if (!models.includes(model)) throw new Error(`cheap-worker: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "cheap-worker.models" with one of: ${models.join(", ")}`);
1867
+ /**
1868
+ * 该轨迹**有没有**轨迹提示词(单一判据)。
1869
+ *
1870
+ * 用途:兼容期的"让位"判定 —— 若已有 `PROMPT.md`,skiff 旧字段 `systemPromptFile`
1871
+ * 必须**让位**,否则同一份章程会在两处各注入一遍(第 ⑧ 段 + `serenity-skiff` 段)。
1872
+ * 🔴 判据只有这一处:别在 skiff 那边自己拼路径再判(两处会漂)。
1873
+ */
1874
+ function hasTrajectoryPrompt(root, dirName) {
1875
+ try {
1876
+ return existsSync(trajectoryPromptPath(root, dirName));
1877
+ } catch {
1878
+ return false;
1879
+ }
1849
1880
  }
1850
1881
  /**
1851
- * providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
1852
- * 返回 `null` = 全部可用(调用方照常继续)。
1882
+ * 轨迹提示词的**读取器**(长期热更新;改文件即生效,无需重启)。
1853
1883
  *
1854
- * 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
1855
- * **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
1856
- * 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
1857
- * 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
1858
- * 而**前台路径的 `diagnostic` 是空的** ⇒ 调用方(LLM)拿到 `stopReason:"error"` 却**不知道为什么**,
1859
- * 也就无法修正自己的配置 —— 本条把「沉默的失败」变成「可执行的错误」。
1884
+ * ## 为什么返回函数而不是字符串(R↓,实证代价)
1860
1885
  *
1861
- * 数据来源(宿主 `LlmRuntime`):`listProviders()` = **有 adapter 的路由**;
1862
- * `listConfigurableProviders()` = **已声明但未必激活**的路由(含激活用的 settings 命名空间)。
1886
+ * 旧实现把提示词**快照**进系统提示词段的闭包 ⇒ CCC 改了文件必须重启 dsh web 才生效
1887
+ * (实测:改完 18KB 提示词,live agent 仍用旧文本)。现形态由系统提示词段的 `text()`
1888
+ * **每轮调用**本读取器:按 **mtime + size** 缓存重读(命中缓存零 IO)。
1863
1889
  *
1864
- * 判据边界:`model` 串里**没有 `/`** 时取不到 provider(沿用 agent 默认路由)⇒ **不判定**(跳过)。
1890
+ * ## 三条失败语义(各治一个失败模式)
1891
+ *
1892
+ * - **文件不存在** ⇒ 返回空串(**不是错误**:普通轨迹没有章程是常态,且删掉即失效
1893
+ * 必须是立即可见的——这就是"删掉 PROMPT.md 即恢复默认"的机制)
1894
+ * - **读取失败** ⇒ 返回**上次成功内容**(⛔ 绝不返回空:空提示词会让轨迹失去章程),
1895
+ * 并按去重标签告警一次
1896
+ * - **首次就读不到** ⇒ 空串 + 告警一次(区分"本来没有"与"有但读不了")
1897
+ *
1898
+ * @param root CCC 根
1899
+ * @param dirName 轨迹目录名(**不是编码**:目录名才是落盘位置)
1900
+ */
1901
+ function createTrajectoryPromptReader(root, dirName) {
1902
+ const absPath = trajectoryPromptPath(root, dirName);
1903
+ let cachedMtime = -1;
1904
+ let cachedSize = -1;
1905
+ let cachedText = "";
1906
+ let warned = false;
1907
+ return () => {
1908
+ let st;
1909
+ try {
1910
+ st = statSync(absPath);
1911
+ } catch (err) {
1912
+ const code = err?.code;
1913
+ cachedMtime = -1;
1914
+ cachedSize = -1;
1915
+ cachedText = "";
1916
+ if (code !== "ENOENT" && !warned) {
1917
+ warned = true;
1918
+ console.warn(`[serenity-hooks] ✗ 轨迹提示词不可读(${absPath})——按"没有章程"处理: ${String(err?.message ?? err)}`);
1919
+ }
1920
+ return "";
1921
+ }
1922
+ try {
1923
+ if (st.mtimeMs === cachedMtime && st.size === cachedSize) return cachedText;
1924
+ const text = readUtf8(absPath).trim();
1925
+ const isReload = cachedMtime !== -1;
1926
+ cachedMtime = st.mtimeMs;
1927
+ cachedSize = st.size;
1928
+ cachedText = text;
1929
+ warned = false;
1930
+ if (isReload) console.info(`[serenity-hooks] ↻ 轨迹提示词已热重载(${TRAJECTORY_PROMPT_FILENAME},${text.length} 字符)`);
1931
+ return text;
1932
+ } catch (err) {
1933
+ if (!warned) {
1934
+ warned = true;
1935
+ console.warn(`[serenity-hooks] ✗ 轨迹提示词读取失败(${absPath})——沿用上次成功内容: ${String(err?.message ?? err)}`);
1936
+ }
1937
+ return cachedText;
1938
+ }
1939
+ };
1940
+ }
1941
+ /**
1942
+ * 装配用的**提示词段**(幂等:无内容 ⇒ 空串,宿主会丢弃空 section/空块)。
1943
+ *
1944
+ * 标记头 `=== Serenity Trajectory Prompt ===`:
1945
+ * ① 让模型知道"下面是**这条轨迹自己的章程**",不是容器规则;
1946
+ * ② 让诊断可以按块名 grep。
1947
+ *
1948
+ * 🔴 这段**追加**在容器层与质量层之后,**永不替换它们**——那正是本概念相对「人格」的核心修正。
1865
1949
  */
1866
- function providerAvailabilityError(models, facts) {
1867
- const registered = new Set(facts.registered.map((r) => r.id));
1868
- const requested = models.map((m) => ({
1869
- raw: m,
1870
- provider: splitModel(m).provider
1871
- })).filter((e) => typeof e.provider === "string" && e.provider !== "");
1872
- const missing = [...new Set(requested.filter((e) => !registered.has(e.provider)).map((e) => e.provider))];
1873
- if (missing.length === 0) return null;
1874
- const list = (items, cap = 12) => items.length === 0 ? " (none)" : items.slice(0, cap).map((s) => ` - ${s}`).join("\n") + (items.length > cap ? `\n … and ${items.length - cap} more` : "");
1875
- const available = facts.registered.map((r) => r.name ? `${r.id} (${r.name})` : r.id).sort();
1876
- const declared = (facts.declared ?? []).filter((d) => missing.includes(d.provider));
1877
- const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
1878
- const missingList = missing.map((p) => `"${p}"`).join(", ");
1950
+ function trajectoryPromptBlock(text) {
1951
+ const body = text.trim();
1952
+ if (body === "") return "";
1879
1953
  return [
1880
- `cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
1881
- "",
1882
- " Requested model(s) :",
1883
- ...requested.map((e) => ` ${e.raw}`),
1884
- ` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
1885
- "",
1886
- " Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
1887
- " provider adapters are registered per machine by the host. An entry that",
1888
- " works on one machine can fail on another.",
1889
- "",
1890
- " Available on this machine (registered adapters):",
1891
- list(available),
1892
- "",
1893
- " Declared but NOT active here (activate via settings, then retry):",
1894
- list(declared.map((d) => `${d.provider}${d.displayName ? ` (${d.displayName})` : ""}${d.settingsNs ? ` — settings namespace "${d.settingsNs}"` : ""}`)),
1895
- "",
1896
- " Fix (pick one, then retry):",
1897
- ` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
1898
- " (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
1899
- ` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
1900
- " or the settings namespace listed above).",
1901
1954
  "",
1902
- " Do not retry unchanged — the same call fails identically."
1955
+ "=== Serenity Trajectory Prompt ===",
1956
+ body,
1957
+ ""
1903
1958
  ].join("\n");
1904
1959
  }
1905
- /**
1906
- * cheap-worker 规模化使用指引(guide 子命令输出)。
1907
- *
1908
- * 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
1909
- * 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
1910
- */
1911
- const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
1912
-
1913
- ## What it is
1914
- One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
1915
- service, awaits them, and returns their final text. There is no loop, no completion code, no progress
1916
- file, and no unattended/background run.
1917
-
1918
- | | single | parallel (jobs) |
1919
- |---|---|---|
1920
- | Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
1921
- | Behaviour | one child runs the task once | all children start at once, results keep your order |
1922
- | Cap | — | **10 (fixed, hardcoded)** |
1923
- | Model | CCC config (single source) | same |
1924
-
1925
- ## ⚠️ Before using: load eap and design the plan
1926
- Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
1927
-
1928
- ### 1. Task decomposition (E↑ Explicit)
1929
- - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
1930
- - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
1931
-
1932
- ### 2. Prompt design (cheap-worker's task parameter)
1933
- - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
1934
- - the child does NOT share your conversation — restate everything it needs in task
1935
- - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
1936
- output a JSON array (fields id/conclusion/evidence), do not modify the original file"
1937
- - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
1938
- the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
1939
-
1940
- ### 3. Model (CCC-configured, not selectable per call)
1941
- - The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
1942
- (there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
1943
- - The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
1944
- - A child's own recursive subagents inherit the child's model automatically (DSH native)
1945
-
1946
- ### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
1947
- - The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
1948
- **adapters are registered per machine by the host** (profile config / settings namespace). An entry that
1949
- works on one machine can fail on another with: "no adapter registered for provider <p>".
1950
- - Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
1951
- **on this machine**; if it is missing it reports the requested model, where it is configured, **the
1952
- providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
1953
- exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
1954
- - ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
1955
- (and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
1956
- machine. **Do not retry the same call unchanged** — it fails identically.
1957
-
1958
- ### 4. Parallel strategy
1959
- - Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
1960
- **in the order you passed them** (label is only a display name).
1961
- - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
1962
-
1963
- ## Completion criteria
1964
- - The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
1965
- (and "output" for whatever partial text was produced).
1966
- `;
1967
1960
  //#endregion
1968
- //#region src/agent-idle.ts
1961
+ //#region src/role/worker/idle.ts
1969
1962
  /**
1970
1963
  * 等待 agent 空闲或销毁。
1971
1964
  * @param ctx 宿主插件上下文(事件通道来源;**结构化访问**,故类型为 `unknown`,见文件头 I2 段)
@@ -2006,9 +1999,9 @@ function waitAgentIdle(ctx, agent) {
2006
1999
  });
2007
2000
  }
2008
2001
  //#endregion
2009
- //#region src/skills-discovery.ts
2002
+ //#region src/skills/discovery.ts
2010
2003
  /**
2011
- * skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
2004
+ * skills/discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
2012
2005
  *
2013
2006
  * 发现全部入口技能(原文全量,不截断)—— **现行三个来源**(原为四个,**来源 3 已于 2026-09-21 删除**):
2014
2007
  * 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义)—— 最高优先
@@ -2112,7 +2105,7 @@ function truncateContent(content, maxChars) {
2112
2105
  * 六条硬性规则(逐条对应设计 §4.2,改这里先改那里):
2113
2106
  * ① **只枚举,不注入正文** —— 索引里**绝不**出现任何参考文件的正文;
2114
2107
  * ② **确定性排序** = 相对路径的 **UTF-8 字节序**(🔴 **不用 mtime**:mtime 随检出漂移,
2115
- * 会让每请求的注入文本抖动;对照 `src/cro.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
2108
+ * 会让每请求的注入文本抖动;对照 `src/wake/cro.ts:101` 那份**落盘快照**用 {name,bytes,mtime} ——
2116
2109
  * **同一件事两处的取舍相反,那个差异是判据不是巧合**);
2117
2110
  * ③ **有界 + 响亮** —— 条数/字符各有上限,超出**必须显式写出还剩多少项**,**绝不静默截断**;
2118
2111
  * ④ **摘要可复现** —— 取该文件首个 `#` 标题(剥 markdown 记号);取不到 ⇒ 留空,**不编**;
@@ -2128,7 +2121,7 @@ function truncateContent(content, maxChars) {
2128
2121
  *
2129
2122
  * 🔴 **两个面,两个入口**(2026-10-12 增;owner 裁决 O2)—— 同源、但成本形态不同:
2130
2123
  * · **载入面** `buildResourceIndex()` —— 完整索引(名字 + 首标题 + 字节),带 `.md` 文件头读取;
2131
- * 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`cognitive-trajectory-skills.ts`
2124
+ * 在三条路径上各附一次:入口段(`system-prompt.ts` order −50)/轨迹段(`trajectory/skills.ts`
2132
2125
  * order −45)/目录 provider 的 `get()`(`opencode-skills.ts`)。
2133
2126
  * · **目录面** `buildResourceNameSuffix()` —— **只有名字**,**不读任何正文**;只挂一处:
2134
2127
  * 目录 provider 的 `list()`(`available_skills` 的 `SkillSummary.description`)。
@@ -2389,9 +2382,9 @@ function buildResourceNameSuffix(skillDir, options = {}) {
2389
2382
  return `(参考件:${names.join(" / ")}${tail})`;
2390
2383
  }
2391
2384
  //#endregion
2392
- //#region src/cognitive-trajectory-skills.ts
2385
+ //#region src/trajectory/skills.ts
2393
2386
  /**
2394
- * cognitive-trajectory-skills.ts — 轨迹 skill 声明解析 + 注入内容装配(纯逻辑,零 DSH 依赖)
2387
+ * trajectory/skills.ts — 轨迹 skill 声明解析 + 注入内容装配(纯逻辑,零 DSH 依赖)
2395
2388
  *
2396
2389
  * 接口真相源:`docs/cognitive-trajectory-skill-injection.md` v1.0(§2 数据契约 / §3 注入契约)。
2397
2390
  * 一句话:**轨迹自己声明它要挂哪些 skill**(`SESSION.md` 顶部 YAML frontmatter 的 `skills:`),
@@ -2589,9 +2582,9 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
2589
2582
  return truncateContent(notice ? `${notice}\n\n${body}` : body, maxChars);
2590
2583
  }
2591
2584
  //#endregion
2592
- //#region src/usage-stats.ts
2585
+ //#region src/surface/usage.ts
2593
2586
  /**
2594
- * usage-stats.ts — ACC 用量统计(**skill 加载次数** + **MSM 执行次数**;落该 CCC 的 `_tmp`)
2587
+ * surface/usage.ts — ACC 用量统计(**skill 加载次数** + **MSM 执行次数**;落该 CCC 的 `_tmp`)
2595
2588
  *
2596
2589
  * ## 所有者令(本模块的存在理由)
2597
2590
  *
@@ -2607,7 +2600,7 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
2607
2600
  * ⇒ 本模块让每个 CCC 留下一份**纯计数**账本:**用了哪个、多少次、第一次和最近一次是什么时候**。
2608
2601
  * 用途 = 清理/精简的**依据**(没有人用的东西与天天在用的东西,不再靠猜)。
2609
2602
  *
2610
- * ## 🔴 铁律(与 `cro-log.ts` 同族)
2603
+ * ## 🔴 铁律(与 `wake/cro-log.ts` 同族)
2611
2604
  *
2612
2605
  * **本模块每个导出函数都不抛错**:读盘 / 解析 / 写盘失败一律返回结构化失败,
2613
2606
  * 由调用方只记一行日志(或什么都不做)⇒ **统计的问题绝不影响工具执行与既有链路**。
@@ -2622,8 +2615,8 @@ function buildCognitiveTrajectorySkillsSection(root, mdPath, maxChars = COGNITIV
2622
2615
  * ## 🔴 重启后必须续算(所有者令)
2623
2616
  *
2624
2617
  * 宿主会被重启(本容器实测 24h 内 3 次)⇒ 计数**不能只活在内存里**:
2625
- * 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake-registry.ts` /
2626
- * `cro-log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
2618
+ * 每次写盘 = **读旧文件 → 在旧值上自增 → 原子写**(`tmp + rename`,与 `wake/registry.ts` /
2619
+ * `wake/cro-log.ts` 同款形态)⇒ **进程重启后计数自然延续**,无需额外的 flush/落盘时机。
2627
2620
  *
2628
2621
  * ## 口径(两个字段,勿合并)
2629
2622
  *
@@ -2906,9 +2899,9 @@ function verifyTotpCode(secretBase32, code, nowSeconds = nowEpochSeconds()) {
2906
2899
  return null;
2907
2900
  }
2908
2901
  //#endregion
2909
- //#region src/config-ops.ts
2902
+ //#region src/kernel/config.ts
2910
2903
  /**
2911
- * config-ops.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
2904
+ * kernel/config.ts — plugin 全局配置读写(结构化,`~/.dsh/serenity-hooks.json`)
2912
2905
  *
2913
2906
  * 归属原则(S142 用户拍板,v1.22):**plugin 是全局的,CCC 是具体的**——
2914
2907
  * 账号密码/gateway 监听配置是 plugin 级能力,归 plugin 全局文件;
@@ -3583,7 +3576,7 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
3583
3576
  * Principles 剥离 MSM 原则段(指令遵循约束)——用户文本承接两处风格;本体论/关系段/
3584
3577
  * 操作边界(安全硬约束)永远保留。未配置 → 与 v1.23.0 逐字节一致(零影响)。
3585
3578
  */
3586
- function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
3579
+ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE, dirName) {
3587
3580
  const persona = readPersonaSettings();
3588
3581
  const personaOn = persona.mode !== "" && persona.overrideText.trim() !== "";
3589
3582
  const parts = [
@@ -3591,12 +3584,18 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
3591
3584
  metaphorBlock(),
3592
3585
  principlesBlock(root, personaOn),
3593
3586
  cceBlock(),
3594
- personaOn ? personaBlock(persona.mode, persona.overrideText) : eapBlock()
3587
+ eapBlock()
3595
3588
  ];
3589
+ if (personaOn) {
3590
+ const expr = personaBlock(persona.mode, persona.overrideText);
3591
+ if (expr !== "") parts.push(expr);
3592
+ }
3596
3593
  const state = [safeModeBlock(root), localstoreBlock(root)].filter((b) => b !== "").join("\n");
3597
3594
  if (state) parts.push(state);
3598
3595
  const skill = entrySkillSectionText(root);
3599
3596
  if (skill) parts.push(skill);
3597
+ const trajPrompt = dirName === void 0 ? "" : trajectoryPromptBlock(createTrajectoryPromptReader(root, dirName)());
3598
+ if (trajPrompt !== "") parts.push(trajPrompt);
3600
3599
  parts.push(toolsBlock());
3601
3600
  const session = sessionBlock(root, scope);
3602
3601
  if (session) parts.push(session);
@@ -3643,6 +3642,23 @@ function agentScope$2(context) {
3643
3642
  return (context.agent?.session)?.id ?? "default";
3644
3643
  }
3645
3644
  /**
3645
+ * 该会话**绑定的轨迹目录名**(有 ⇒ 注入第 ⑧ 段「轨迹提示词」;无 ⇒ `undefined`,不注入)。
3646
+ *
3647
+ * 🔴 为什么用**目录名**而不是编码或 label:`PROMPT.md` 的落盘位置是目录,
3648
+ * 而编码只是目录名里的一段。用 label 拼路径 = 把模糊匹配当精确匹配用(本轨已踩过)。
3649
+ *
3650
+ * ⚠️ 每次装配都重读绑定(**不缓存**):绑定切换后提示词必须立刻跟上,
3651
+ * 这与 `serenity-trajectory-skills` section 的口径一致(动态注入,owner 令 2026-09-20)。
3652
+ */
3653
+ function boundDirNameFor(session) {
3654
+ try {
3655
+ const name = readLastBound(session)?.dirName;
3656
+ return typeof name === "string" && name !== "" ? name : void 0;
3657
+ } catch {
3658
+ return;
3659
+ }
3660
+ }
3661
+ /**
3646
3662
  * 全局注册系统提示词 section(osp system.transform 的 DSH 等价)。
3647
3663
  * text 回调按 context.agent 的 cwd 上溯 .serenity → 返回该 CCC 的完整注入文本;
3648
3664
  * 非 CCC / 无 agent 返回空(不注入)。
@@ -3657,9 +3673,7 @@ function registerEntrySkillSectionGlobal(ctx) {
3657
3673
  if (!cwd) return "";
3658
3674
  const root = cccRootForCwd(cwd);
3659
3675
  if (!root) return "";
3660
- const scope = agentScope$2(context);
3661
- if (isSkiffSessionId(scope)) return "";
3662
- const base = serenitySystemPrompt(root, scope);
3676
+ const base = serenitySystemPrompt(root, agentScope$2(context), boundDirNameFor(context.agent?.session));
3663
3677
  const codeLine = codeModeAdaptationLine(ctx, context.scope);
3664
3678
  return codeLine ? `${base}\n${codeLine}` : base;
3665
3679
  }
@@ -3691,8 +3705,7 @@ function registerEntrySkillSection(agent, root) {
3691
3705
  name: "serenity-entry",
3692
3706
  order: -50,
3693
3707
  text: (context) => {
3694
- if (isSkiffSessionId(scope)) return "";
3695
- const base = serenitySystemPrompt(root, scope);
3708
+ const base = serenitySystemPrompt(root, scope, boundDirNameFor(agent.session));
3696
3709
  const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
3697
3710
  return codeLine ? `${base}\n${codeLine}` : base;
3698
3711
  }
@@ -3712,7 +3725,7 @@ function registerEntrySkillSection(agent, root) {
3712
3725
  * · **轨迹级** = 该会话绑定的轨迹(`readLastBound(session).mdPath` = 它的 `SESSION.md`)
3713
3726
  * 顶部 frontmatter 的 `skills:`——这条轨迹额外的。
3714
3727
  * 合并规则与顺序见 `cognitive-trajectory-skills.mergeSkillNames`(CCC 级在前,同名去重)。
3715
- * 正文 = 合并清单里各 skill 的 `SKILL.md` 全文(本模块只注册,装配在 `cognitive-trajectory-skills.ts`)。
3728
+ * 正文 = 合并清单里各 skill 的 `SKILL.md` 全文(本模块只注册,装配在 `trajectory/skills.ts`)。
3716
3729
  *
3717
3730
  * 为什么是 **per-agent scoped section**(与 `registerEntrySkillSection` 同形):注入必须
3718
3731
  * **绑定期间持续存在**(a2)——一次性注入会随对话压缩消失,而 section 每次装配重新求值。
@@ -3755,13 +3768,152 @@ function registerCognitiveTrajectorySkillSection(agent, root) {
3755
3768
  }
3756
3769
  }
3757
3770
  //#endregion
3758
- //#region src/skiff-core.ts
3759
- const skiffAgents = /* @__PURE__ */ new Map();
3760
- /** 查 sessionId 的 Skiff 角色名(无 → null) */
3761
- function skiffRoleFor(sessionId) {
3762
- return skiffRoleFor$1(sessionId);
3771
+ //#region src/role/worker/core.ts
3772
+ /**
3773
+ * role/worker/core.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
3774
+ *
3775
+ * 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
3776
+ * 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
3777
+ * 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
3778
+ */
3779
+ /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
3780
+ function splitModel(model) {
3781
+ const idx = model.indexOf("/");
3782
+ if (idx < 0) return {
3783
+ provider: void 0,
3784
+ model
3785
+ };
3786
+ return {
3787
+ provider: model.slice(0, idx),
3788
+ model: model.slice(idx + 1)
3789
+ };
3790
+ }
3791
+ /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
3792
+ function requireWhitelistedModel(model, models) {
3793
+ if (!models.includes(model)) throw new Error(`cheap-worker: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "cheap-worker.models" with one of: ${models.join(", ")}`);
3763
3794
  }
3764
- /** 查 sessionId 的会话绑定(role + ccc;无 → null)——v1.25.10 追问校验 */
3795
+ /**
3796
+ * providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
3797
+ * 返回 `null` = 全部可用(调用方照常继续)。
3798
+ *
3799
+ * 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
3800
+ * **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
3801
+ * 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
3802
+ * 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
3803
+ * 而**前台路径的 `diagnostic` 是空的** ⇒ 调用方(LLM)拿到 `stopReason:"error"` 却**不知道为什么**,
3804
+ * 也就无法修正自己的配置 —— 本条把「沉默的失败」变成「可执行的错误」。
3805
+ *
3806
+ * 数据来源(宿主 `LlmRuntime`):`listProviders()` = **有 adapter 的路由**;
3807
+ * `listConfigurableProviders()` = **已声明但未必激活**的路由(含激活用的 settings 命名空间)。
3808
+ *
3809
+ * 判据边界:`model` 串里**没有 `/`** 时取不到 provider(沿用 agent 默认路由)⇒ **不判定**(跳过)。
3810
+ */
3811
+ function providerAvailabilityError(models, facts) {
3812
+ const registered = new Set(facts.registered.map((r) => r.id));
3813
+ const requested = models.map((m) => ({
3814
+ raw: m,
3815
+ provider: splitModel(m).provider
3816
+ })).filter((e) => typeof e.provider === "string" && e.provider !== "");
3817
+ const missing = [...new Set(requested.filter((e) => !registered.has(e.provider)).map((e) => e.provider))];
3818
+ if (missing.length === 0) return null;
3819
+ const list = (items, cap = 12) => items.length === 0 ? " (none)" : items.slice(0, cap).map((s) => ` - ${s}`).join("\n") + (items.length > cap ? `\n … and ${items.length - cap} more` : "");
3820
+ const available = facts.registered.map((r) => r.name ? `${r.id} (${r.name})` : r.id).sort();
3821
+ const declared = (facts.declared ?? []).filter((d) => missing.includes(d.provider));
3822
+ const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
3823
+ const missingList = missing.map((p) => `"${p}"`).join(", ");
3824
+ return [
3825
+ `cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
3826
+ "",
3827
+ " Requested model(s) :",
3828
+ ...requested.map((e) => ` ${e.raw}`),
3829
+ ` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
3830
+ "",
3831
+ " Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
3832
+ " provider adapters are registered per machine by the host. An entry that",
3833
+ " works on one machine can fail on another.",
3834
+ "",
3835
+ " Available on this machine (registered adapters):",
3836
+ list(available),
3837
+ "",
3838
+ " Declared but NOT active here (activate via settings, then retry):",
3839
+ list(declared.map((d) => `${d.provider}${d.displayName ? ` (${d.displayName})` : ""}${d.settingsNs ? ` — settings namespace "${d.settingsNs}"` : ""}`)),
3840
+ "",
3841
+ " Fix (pick one, then retry):",
3842
+ ` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
3843
+ " (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
3844
+ ` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
3845
+ " or the settings namespace listed above).",
3846
+ "",
3847
+ " Do not retry unchanged — the same call fails identically."
3848
+ ].join("\n");
3849
+ }
3850
+ /**
3851
+ * cheap-worker 规模化使用指引(guide 子命令输出)。
3852
+ *
3853
+ * 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
3854
+ * 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
3855
+ */
3856
+ const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
3857
+
3858
+ ## What it is
3859
+ One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
3860
+ service, awaits them, and returns their final text. There is no loop, no completion code, no progress
3861
+ file, and no unattended/background run.
3862
+
3863
+ | | single | parallel (jobs) |
3864
+ |---|---|---|
3865
+ | Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
3866
+ | Behaviour | one child runs the task once | all children start at once, results keep your order |
3867
+ | Cap | — | **10 (fixed, hardcoded)** |
3868
+ | Model | CCC config (single source) | same |
3869
+
3870
+ ## ⚠️ Before using: load eap and design the plan
3871
+ Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
3872
+
3873
+ ### 1. Task decomposition (E↑ Explicit)
3874
+ - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
3875
+ - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
3876
+
3877
+ ### 2. Prompt design (cheap-worker's task parameter)
3878
+ - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
3879
+ - the child does NOT share your conversation — restate everything it needs in task
3880
+ - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
3881
+ output a JSON array (fields id/conclusion/evidence), do not modify the original file"
3882
+ - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
3883
+ the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
3884
+
3885
+ ### 3. Model (CCC-configured, not selectable per call)
3886
+ - The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
3887
+ (there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
3888
+ - The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
3889
+ - A child's own recursive subagents inherit the child's model automatically (DSH native)
3890
+
3891
+ ### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
3892
+ - The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
3893
+ **adapters are registered per machine by the host** (profile config / settings namespace). An entry that
3894
+ works on one machine can fail on another with: "no adapter registered for provider <p>".
3895
+ - Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
3896
+ **on this machine**; if it is missing it reports the requested model, where it is configured, **the
3897
+ providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
3898
+ exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
3899
+ - ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
3900
+ (and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
3901
+ machine. **Do not retry the same call unchanged** — it fails identically.
3902
+
3903
+ ### 4. Parallel strategy
3904
+ - Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
3905
+ **in the order you passed them** (label is only a display name).
3906
+ - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
3907
+
3908
+ ## Completion criteria
3909
+ - The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
3910
+ (and "output" for whatever partial text was produced).
3911
+ `;
3912
+ //#endregion
3913
+ //#region src/role/skiff/core.ts
3914
+ /** `provider/model` → agentOptions(由 worker/core 提供,保持单一实现) */
3915
+ const skiffAgents = /* @__PURE__ */ new Map();
3916
+ /** 查 sessionId 的会话绑定(轨迹 + ccc;无 → null)——追问校验 */
3765
3917
  function skiffSessionInfo(sessionId) {
3766
3918
  return skiffSessionInfo$1(sessionId);
3767
3919
  }
@@ -3769,76 +3921,80 @@ function skiffSessionInfo(sessionId) {
3769
3921
  function getSkiffAgent(sessionId) {
3770
3922
  return skiffAgents.get(sessionId);
3771
3923
  }
3772
- function registerSkiffSession(sessionId, role, ccc, agent) {
3924
+ /** 该会话当前绑定的 skiff 约束面(不是 skiff → null)。**守门与提示词的唯一查询入口。** */
3925
+ function skiffProfileFor(root, sessionId) {
3926
+ if (sessionId === void 0) return null;
3927
+ return skiffProfileForSession(root, sessionId, skiffTrajectoryDirNameFor(sessionId));
3928
+ }
3929
+ function registerSkiffSession(sessionId, trajectoryDirName, ccc, agent) {
3773
3930
  skiffAgents.set(sessionId, agent);
3774
- registerSkiffSession$1(sessionId, role, ccc);
3931
+ registerSkiffSession$1(sessionId, trajectoryDirName, ccc);
3775
3932
  }
3776
3933
  function unregisterSkiffSession(sessionId) {
3777
3934
  skiffAgents.delete(sessionId);
3778
3935
  unregisterSkiffSession$1(sessionId);
3779
3936
  }
3780
- /** 自动创建的 bound 标记(readLastBound 恢复判定——只恢复本机制建的;手工 use 的旧 bound 不认) */
3781
- const AUTO_BOUND_NOTE_PREFIX = "auto-created for skiff role";
3782
3937
  /**
3783
- * 系统提示词中的**工作台纪律块**(v1.30.4 升级:从单行路径 → 完整 ACC 层约束)。
3784
- * 用户点破(S142):只给路径 LLM 不会主动用——需注入「已绑定 + 使用纪律 + 动作指引」。
3785
- * SESSION = 工作台——skiff 与主舱同一套机制(零特调,用户拍板):
3786
- * 自动绑定已生效(无需 cognitive_trajectory use)、SESSION.md 是持久记忆载体、rebuild 自动从本 SESSION 续接。
3938
+ * 该 skiff 会话的**工作台路径** = 它自己轨迹的 SESSION.md。
3939
+ *
3940
+ * 🔴 与旧版的差别:旧版会在这里**自动新建**一个 SESSION(`ensureSkiffSession`);
3941
+ * 新版**没有新建这个动作** —— 轨迹就是工作台,`skiff.json` 与 `SESSION.md` 同处一个目录。
3942
+ * ⇒ 语义变成纯粹的"查询:我自己的身体在哪"。
3943
+ *
3944
+ * @returns SESSION.md 绝对路径;不是 skiff / 文件不在 → null
3945
+ */
3946
+ function skiffWorkspaceMdPath(root, sessionId) {
3947
+ if (sessionId === void 0) return null;
3948
+ const dirName = skiffTrajectoryDirNameFor(sessionId);
3949
+ if (dirName === null) return null;
3950
+ const md = join(sessionsRoot(root), dirName, "SESSION.md");
3951
+ return existsSync(md) ? md : null;
3952
+ }
3953
+ /**
3954
+ * 系统提示词中的**工作台纪律块**(S142 用户:只给路径 LLM 不会主动用)。
3955
+ *
3956
+ * SESSION = 工作台 —— skiff 与主舱同一套机制(**零特调**,用户拍板):
3957
+ * SESSION.md 是持久记忆载体、rebuild 自动从本 SESSION 续接。
3787
3958
  * 注入 agent 系统提示词(用户对话面不可见)。
3788
3959
  *
3789
- * v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具**。旧文案写死 "with write/edit",
3790
- * 而某角色的工具白名单已移除 write/edit(写能力收归 CCC 的 session-write MSM)
3791
- * → 指令指向它根本没有的工具(注入纪律与能力面矛盾,模型只能猜)。
3792
- * 现文案改为「用本角色被授权的写入通道」——ACC 只声明义务(写进工作台),
3793
- * 具体通道由 CCC 的角色配置与角色提示词决定(ACC/CCC 归属二分;也不把某个 CCC 的
3794
- * MSM 名硬编码进 ACC 源码)。
3960
+ * v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具** —— 旧文案写死 "with write/edit",
3961
+ * 而某个 skiff 的工具白名单里根本没有 write/edit ⇒ 指令指向它没有的工具
3962
+ * (注入纪律与能力面矛盾,模型只能猜)。现文案改为「用本轨迹被授权的写入通道」。
3795
3963
  */
3796
3964
  function workspaceCognitiveTrajectoryLine(mdPath) {
3797
3965
  return [
3798
3966
  "── Serenity Session Workspace ──",
3799
- "This role session is AUTO-BOUND to a cognitive trajectory workspace SESSION (no cognitive_trajectory use needed):",
3967
+ "You ARE a cognitive trajectory: this workspace SESSION is your own persistent body.",
3800
3968
  ` SESSION.md: ${mdPath}`,
3801
3969
  "",
3802
3970
  "Rules of the workspace:",
3803
3971
  " 1. This SESSION.md is your persistent memory carrier (like the main cabin SESSION) — ",
3804
3972
  " read it first when context feels thin or work spans turns.",
3805
3973
  " 2. Record key decisions/progress/unresolved items INTO this SESSION.md using the write channel",
3806
- " your role is actually granted (use whatever write tool or MSM this role has — never assume",
3974
+ " your trajectory is actually granted (use whatever write tool or MSM you have — never assume",
3807
3975
  " a specific write tool exists; if none is granted, report that instead of pretending to write).",
3808
3976
  " Sections: 身份与边界 / 权威锚点 / 工作纪律 / 决策 / 日志 / 当前状态 / 待 owner 裁决.",
3809
3977
  " Keep entries at milestone level (one line + evidence pointer); execution detail",
3810
- " belongs in commit messages. Keep it current — it is",
3811
- " what a rebuild resumes from.",
3812
- " 3. When context pressure is high, run cognitive_trajectory rebuild — it auto-resumes from THIS SESSION.md",
3813
- " (no manual use; do not switch to or read other SESSIONs unless the user explicitly asks).",
3978
+ " belongs in commit messages. Keep it current — it is what a rebuild resumes from.",
3979
+ " 3. When context pressure is high, run cognitive_trajectory rebuild — it auto-resumes from THIS SESSION.md.",
3814
3980
  " 4. Never expose this internal path in user-facing replies (chat stays clean).",
3815
3981
  "── ──"
3816
3982
  ].join("\n");
3817
3983
  }
3818
- /**
3819
- * 已挂工作台提示词段的 agent(幂等:同一 agent 只注册一次,避免 duplicate section)。
3820
- * WeakSet → agent 被回收即自动清理,无泄漏。
3821
- */
3984
+ /** 已挂工作台提示词段的 agent(幂等:同一 agent 只注册一次,避免 duplicate section) */
3822
3985
  const workspaceSectionAgents = /* @__PURE__ */ new WeakSet();
3823
3986
  /**
3824
- * 把**工作台纪律块**挂到 agent 的系统提示词段(单一真相源,v1.30.13 用户需求)。
3987
+ * 把**工作台纪律块**挂到 agent 的系统提示词段(单一注入点)。
3825
3988
  *
3826
3989
  * 用户原话(S142,2026-09-08):"微信桥的注入机制,每个用户消息都会注入,skiff 本身也会注入,
3827
3990
  * 这样就重复,能否微信桥情况下,注入内容直接取 skiff 的,这样不用配两遍"。
3828
3991
  *
3829
- * 现状(改前):`createSkiffAgent` 把 `workspaceCognitiveTrajectoryLine(mdPath)` 塞进基础段
3830
- * (创建时一次快照),微信桥又**每条消息**把同一文本拼进 question(v1.30.4 为覆盖
3831
- * "live/existing 快路径不重挂提示词"而加)→ 新建 agent 首轮起即重复,且每轮重复付 token。
3992
+ * 现形态:工作台行**只**经本函数挂系统提示词段(name `serenity-skiff-workspace`,
3993
+ * 动态 `text()` 每轮按 scope 读当前活跃 mdPath)。
3832
3994
  *
3833
- * 现形态(单一注入点):工作台行**只**经本函数挂系统提示词段(name `serenity-skiff-workspace`,
3834
- * 动态 `text()` 每轮按 scope 读当前活跃 mdPath → 绑定变化自动跟随,无需重挂):
3835
- * - `createSkiffAgent` 调它(不再把工作台行拼进基础段);
3836
- * - 微信桥对 live/existing agent 调它(不再拼 question)。
3837
- *
3838
- * @returns true = 段已在位(本次注册成功或此前已注册);false = 该 agent 无法挂系统提示词段
3839
- * (宿主 systemPrompt 缺失/注册抛错)→ 调用方降级为 question 前缀注入(约束必须在场)
3995
+ * @returns true = 段已在位;false = 该 agent 挂不上(调用方降级为 question 前缀注入)
3840
3996
  */
3841
- function ensureWorkspacePromptSection(agent, scope) {
3997
+ function ensureWorkspacePromptSection(agent, scope, root) {
3842
3998
  if (!agent || typeof agent !== "object") return false;
3843
3999
  if (workspaceSectionAgents.has(agent)) return true;
3844
4000
  const api = agent.ctx?.systemPrompt;
@@ -3848,7 +4004,7 @@ function ensureWorkspacePromptSection(agent, scope) {
3848
4004
  name: "serenity-skiff-workspace",
3849
4005
  order: -55,
3850
4006
  text: () => {
3851
- const mdPath = getActiveSessionInfo(scope)?.mdPath;
4007
+ const mdPath = skiffWorkspaceMdPath(root, scope);
3852
4008
  return mdPath ? workspaceCognitiveTrajectoryLine(mdPath) : "";
3853
4009
  }
3854
4010
  });
@@ -3859,90 +4015,22 @@ function ensureWorkspacePromptSection(agent, scope) {
3859
4015
  return false;
3860
4016
  }
3861
4017
  }
3862
- /**
3863
- * 确保启用 session 能力的 skiff 会话拥有**专属 SESSION**(工作台):
3864
- * 懒绑定 + 幂等(per skiff 会话 scope 隔离——微信多用户各自独立,永不共享同一份)。
3865
- *
3866
- * 判定链:
3867
- * ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
3868
- * (调试页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
3869
- * 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
3870
- * 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
3871
- * ① 角色未启用 session 能力(cognitiveTrajectory.session !== true)→ null(现状零变化)
3872
- * ② 已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
3873
- * ②b 内存活跃记录(同 scope)→ 恢复 + **自愈补写绑定**(防御纵深:绑定写失败/事件形态未落盘时
3874
- * 仍复用同一 SESSION,而不是每条消息新建一个——v1.30.5 及更早"每条消息一个 SESSION"的放大效应)
3875
- * ③ 无 → 自动创建专属 SESSION(createSession desc=`<role> skiff`——编号自动递增,用户不关心是谁)
3876
- *
3877
- * @param persistent 该 skiff 会话是否有**持久身份**(固定会话 id)。默认 true(向后兼容调用点);
3878
- * `createSkiffAgent` 传 `sessionId !== undefined`,微信桥的 existing 快路径传 true。
3879
- * @returns 绑定后的 SESSION.md 绝对路径(供提示词注入);不适用/失败 → null
3880
- */
3881
- function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
3882
- if (!cognitiveTrajectorySubset(role).session) return null;
3883
- if (!persistent) return null;
3884
- const session = agent.session;
3885
- const scope = String(agent.session.id ?? agent.id);
3886
- const lastBound = session ? readLastBound(session) : null;
3887
- if (lastBound && lastBound.note?.startsWith(AUTO_BOUND_NOTE_PREFIX) && existsSync(lastBound.mdPath)) {
3888
- setActiveSessionInfo(scope, {
3889
- sessionId: lastBound.sessionId ?? "",
3890
- dirName: lastBound.dirName,
3891
- mdPath: lastBound.mdPath
3892
- });
3893
- return lastBound.mdPath;
3894
- }
3895
- const inMemory = getActiveSessionInfo(scope);
3896
- if (inMemory?.mdPath && existsSync(inMemory.mdPath)) {
3897
- appendBound(session, "reconcile", {
3898
- dirName: inMemory.dirName,
3899
- mdPath: inMemory.mdPath,
3900
- ...inMemory.sessionId ? { sessionId: inMemory.sessionId } : {},
3901
- note: `${AUTO_BOUND_NOTE_PREFIX} ${roleName}`
3902
- });
3903
- return inMemory.mdPath;
3904
- }
3905
- const desc = `${roleName} skiff`;
3906
- const created = createSession({
3907
- root,
3908
- desc,
3909
- dryRun: false
3910
- });
3911
- const mdPath = join(created.sessionPath, "SESSION.md");
3912
- setActiveSessionInfo(scope, {
3913
- sessionId: created.sessionId,
3914
- dirName: created.dirName,
3915
- mdPath
3916
- });
3917
- appendBound(session, "create", {
3918
- dirName: created.dirName,
3919
- mdPath,
3920
- sessionId: created.sessionId,
3921
- note: `${AUTO_BOUND_NOTE_PREFIX} ${roleName}`
3922
- });
3923
- return mdPath;
3924
- }
3925
- /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
3926
- * cheap-worker 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
3927
- * guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
4018
+ /** skiff agent 挂载的 DSH preset(平台工具面由 preset 决定;白名单再按轨迹过滤) */
3928
4019
  const SKIFF_PRESET = "standard";
3929
4020
  /**
3930
- * 创建 Skiff agent:标准 DSH agent + cwd=CCC root + 角色模型 +
3931
- * standard preset(平台工具面)+ scoped 系统提示词(基础提示词 + CCC 定义段,全替换 ACC 默认注入)。
4021
+ * 创建(或复用)一个 skiff 会话。
3932
4022
  *
3933
- * **固定 sessionId 的 resume-or-create(v1.27.2,微信桥 id collision 修复)**:
3934
- * 指定 sessionId 且磁盘已有持久化 log → `ctx.agents.resume`(DSH 持久化语义:
3935
- * sessionId 即身份,已持久化会话必须 resume——历史 + turn 续号延续,同用户长期记忆保留);
3936
- * 无 log(首次)/持久化未配置 → `ctx.agents.create`(新建)。不指定 sessionId →
3937
- * 随机 id 恒 create(调试页默认路径)。
4023
+ * **固定 sessionId 的 resume-or-create**(v1.27.2):指定 sessionId 且磁盘已有持久化 log →
4024
+ * `ctx.agents.resume`(DSH 持久化语义:sessionId 即身份,历史 + turn 续号延续);
4025
+ * 无 log(首次)→ `ctx.agents.create`。不指定 → 随机 id 恒 create(调试页路径)。
3938
4026
  *
3939
- * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
3940
- * 不传则随机生成 skiff-<role>-<uuid>,调试页默认路径)
4027
+ * @param profile 该 skiff 的约束面(来自 `skiff.json`)
4028
+ * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现"轨迹 ↔ 会话"长期映射)
3941
4029
  */
3942
- async function createSkiffAgent(ctx, root, roleName, role, defaultModel, sessionId) {
4030
+ async function createSkiffAgent(ctx, root, profile, defaultModel, sessionId) {
3943
4031
  if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
3944
- const model = role.model?.trim() || defaultModel || "";
3945
- const id = sessionId ?? `skiff-${roleName}-${randomUUID()}`;
4032
+ const model = profile.model?.trim() || defaultModel || "";
4033
+ const id = sessionId ?? `skiff-${profile.code}-${randomUUID()}`;
3946
4034
  const setup = async (agentCtx) => {
3947
4035
  try {
3948
4036
  await agentCtx.get("agentPresets")?.mount?.(agentCtx, SKIFF_PRESET);
@@ -3950,49 +4038,30 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
3950
4038
  };
3951
4039
  const handle = await createOrResumeAgent(ctx, id, root, model, setup, sessionId !== void 0);
3952
4040
  const agent = handle.agent;
3953
- const kind = resolveSkiffKind(role, sessionId !== void 0);
3954
- const rolePrompt = kind === "persistent" ? createRolePromptReader(root, role) : (() => {
3955
- let snapshot = "";
4041
+ let readerPath = profile.systemPromptFile?.trim();
4042
+ let reader = createProfilePromptReader(root, profile);
4043
+ const livePrompt = () => {
4044
+ let live = profile;
3956
4045
  try {
3957
- snapshot = resolveRoleSystemPrompt(root, role);
3958
- } catch (err) {
3959
- console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 系统提示词解析失败(回退仅基础段): ${String(err?.message ?? err)}`);
4046
+ const fresh = readSkiffProfileAt(join(sessionsRoot(root), profile.dirName), root);
4047
+ if (fresh) live = fresh;
4048
+ } catch {}
4049
+ const wantPath = live.systemPromptFile?.trim();
4050
+ if (wantPath !== readerPath) {
4051
+ reader = createProfilePromptReader(root, live);
4052
+ readerPath = wantPath;
3960
4053
  }
3961
- return () => snapshot;
3962
- })();
3963
- rolePrompt();
3964
- let workspaceMdPath = null;
3965
- try {
3966
- workspaceMdPath = ensureSkiffSession(root, agent, roleName, role, kind === "persistent");
3967
- } catch (err) {
3968
- console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 专属 SESSION 处理失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
3969
- }
4054
+ return reader();
4055
+ };
4056
+ livePrompt();
3970
4057
  try {
3971
- let readerPath = role?.systemPromptFile?.trim();
3972
- let reader = rolePrompt;
3973
- const liveRolePrompt = () => {
3974
- let live = role;
3975
- try {
3976
- const fresh = readSkiffRoles(root).get(roleName);
3977
- if (fresh) live = fresh;
3978
- } catch {}
3979
- const wantPath = live?.systemPromptFile?.trim();
3980
- if (wantPath !== readerPath) {
3981
- reader = kind === "persistent" ? createRolePromptReader(root, live) : (() => {
3982
- let snapshot = "";
3983
- try {
3984
- snapshot = resolveRoleSystemPrompt(root, live);
3985
- } catch {}
3986
- return () => snapshot;
3987
- })();
3988
- readerPath = wantPath;
3989
- }
3990
- return reader();
3991
- };
3992
4058
  agent.ctx.systemPrompt.section({
3993
4059
  name: "serenity-skiff",
3994
4060
  order: -60,
3995
- text: () => [buildSkiffBasePrompt(roleName), liveRolePrompt()].filter(Boolean).join("\n")
4061
+ text: () => {
4062
+ const legacy = hasTrajectoryPrompt(root, profile.dirName) ? "" : livePrompt();
4063
+ return [buildSkiffBasePrompt(profile.code), legacy].filter(Boolean).join("\n");
4064
+ }
3996
4065
  });
3997
4066
  } catch (err) {
3998
4067
  console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
@@ -4002,8 +4071,8 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
4002
4071
  } catch (err) {
4003
4072
  console.warn(`[serenity-hooks] skiff 轨迹 skill 注入注册失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
4004
4073
  }
4005
- if (workspaceMdPath) ensureWorkspacePromptSection(agent, id);
4006
- registerSkiffSession(id, roleName, root, agent);
4074
+ if (skiffWorkspaceMdPath(root, id) !== null) ensureWorkspacePromptSection(agent, id, root);
4075
+ registerSkiffSession(id, profile.dirName, root, agent);
4007
4076
  return {
4008
4077
  handle,
4009
4078
  agent,
@@ -4029,14 +4098,12 @@ function liveReuseRef(live, setup) {
4029
4098
  }
4030
4099
  /**
4031
4100
  * resume-or-create 分派:固定 id → 优先 resume(持久化历史延续),失败降级 create;
4032
- * 随机 id(无固定 sessionId)→ 恒 create。返回 handle + resumed 标志。
4101
+ * 随机 id(无固定 sessionId)→ 恒 create。
4033
4102
  *
4034
4103
  * **v1.27.3 修复(微信桥"重启后不响应"根因):live 会话优先复用**——重启后 DSH 把
4035
- * 关闭时仍 live 的持久化会话恢复为 live(crash/restart 恢复语义)。此时 resume 报
4036
- * "cannot prepare session X while it is live"(session-persistence coordinator 显式
4037
- * 拒绝 live 会话)、create 报 "session X already exists"(磁盘有持久化 log 必拒)→
4038
- * 微信桥 catch 吞错 → 用户消息无回复。唯一可行路径 = `ctx.agents.get(id)` 取 live
4039
- * agent 直接续用(resumed:true;角色提示词重挂与内存注册表登记由 createSkiffAgent 统一做)。
4104
+ * 关闭时仍 live 的持久化会话恢复为 live。此时 resume 报
4105
+ * "cannot prepare session X while it is live"、create 报 "session X already exists"
4106
+ * ⇒ 唯一可行路径 = `ctx.agents.get(id)` 取 live agent 直接续用。
4040
4107
  */
4041
4108
  async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
4042
4109
  if (!fixedId) return {
@@ -4096,18 +4163,13 @@ async function createOrResumeAgent(ctx, id, root, model, setup, fixedId) {
4096
4163
  };
4097
4164
  }
4098
4165
  }
4099
- /**
4100
- * 会话事件读取(v1.28.0 适配 0.1.2-rc.1):rc.1 Session 移除 `.events` 属性 → `snapshotEvents()` 方法。
4101
- * 兼容双形态(运行时 rc.1 snapshotEvents;测试替身 events)——同一函数两处语义,测试注入零侵入。
4102
- * 注:与 cognitive-trajectory-ops.ts 导出的共享 `sessionEvents` 同构(v1.28.1 收敛基准)。本模块保留本地副本
4103
- * 以维持独立 import 面(零 DSH 依赖测试替身形态一致);若未来重构可统一 import 共享版。
4104
- */
4166
+ /** 会话事件读取(兼容 `snapshotEvents()` 与测试替身的 `.events`) */
4105
4167
  function sessionEvents(session) {
4106
4168
  const s = session;
4107
4169
  if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
4108
4170
  return s.events ?? [];
4109
4171
  }
4110
- /** 读会话最后一个 assistant/message 文本(cheap-worker 同款) */
4172
+ /** 读会话最后一个 assistant/message 文本 */
4111
4173
  function lastAssistantText$1(agent) {
4112
4174
  const events = sessionEvents(agent.session);
4113
4175
  for (let i = events.length - 1; i >= 0; i--) {
@@ -4119,62 +4181,45 @@ function lastAssistantText$1(agent) {
4119
4181
  }
4120
4182
  return "";
4121
4183
  }
4122
- /** events → 可读轨迹(user/assistant 文本 + 工具调用 + 工具结果;单条解析失败跳过) */
4123
- function eventsToCognitiveTrajectory(events) {
4124
- const out = [];
4125
- for (const raw of events) try {
4126
- const ev = raw;
4127
- if (ev.type === "user/message") {
4128
- const text = extractText(ev.data);
4129
- if (text) out.push({
4130
- role: "user",
4131
- text
4132
- });
4133
- } else if (ev.type === "assistant/message") {
4134
- const d = ev.data;
4135
- const text = (d?.message?.content ?? d?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
4136
- const calls = d?.message?.tool_calls ?? [];
4137
- if (text) out.push({
4138
- role: "assistant",
4139
- text
4140
- });
4141
- for (const c of calls ?? []) {
4142
- const args = typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {});
4143
- out.push({
4144
- role: "assistant",
4145
- text: `→ ${c.name ?? "(tool)"} ${truncate(args, 300)}`,
4146
- tool: c.name
4147
- });
4148
- }
4149
- } else if (ev.type === "tool/result") {
4150
- const outText = extractText(ev.data);
4151
- if (outText) out.push({
4152
- role: "tool",
4153
- text: truncate(outText, 500),
4154
- tool: String(ev.data?.name ?? "")
4155
- });
4156
- }
4157
- } catch {}
4158
- return out;
4159
- }
4160
- function extractText(data) {
4161
- const content = data?.content;
4162
- if (!Array.isArray(content)) return "";
4163
- return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
4184
+ /** 本轮等待上限(毫秒):超时即判「本轮无产出」——**绝不**把上一轮的旧答案当本轮答案 */
4185
+ const ASK_TURN_TIMEOUT_MS = 18e4;
4186
+ /** 会话里 `assistant/message` 的条数(判「本轮是否真的产出了回答」) */
4187
+ function countAssistantMessages(session) {
4188
+ let n = 0;
4189
+ for (const e of sessionEvents(session)) if (e?.type === "assistant/message") n += 1;
4190
+ return n;
4164
4191
  }
4165
- function truncate(s, n) {
4166
- return s.length > n ? `${s.slice(0, n)}…` : s;
4192
+ /**
4193
+ * 等**本轮**产出一条 assistant 消息。
4194
+ *
4195
+ * ## 为什么不能用 `waitAgentIdle` 单独当判据(2026-10-19 复读缺陷)
4196
+ *
4197
+ * `waitAgentIdle` 的首行是 `if (agent.status === 'idle') finish()` —— 而 `followup()`
4198
+ * 是**异步投递**:刚投出去的这一刻 agent 状态**仍是 idle** ⇒ 等待**立刻结算** ⇒
4199
+ * `lastAssistantText()` 读到的是**上一轮的** `assistant/message` ⇒ 桥把它当"本轮答案"转发。
4200
+ * 若 CCC 又开了 `fallbackOnNoSend`,同一条旧答案会被反复重发(实测三次间隔 300~400ms、逐字相同)。
4201
+ *
4202
+ * ## 判据(`idle` 只是"可以开始看"的信号,不是成功信号)
4203
+ *
4204
+ * ① 期望值 = 调用前 `assistant/message` 的**条数**;
4205
+ * ② 成功 ⟺ 该计数**增长**;
4206
+ * ③ 超时或有界轮询耗尽 ⇒ `false`(调用方据此给空答案,兜底不触发)。
4207
+ */
4208
+ async function waitForTurnAssistant(ctx, agent, beforeAssistantCount, opts = {}) {
4209
+ const timeoutMs = opts.timeoutMs ?? ASK_TURN_TIMEOUT_MS;
4210
+ const grew = () => countAssistantMessages(agent.session) > beforeAssistantCount;
4211
+ if (await Promise.race([waitAgentIdle(ctx, agent).then(() => true), new Promise((resolve) => {
4212
+ setTimeout(() => resolve(false), timeoutMs);
4213
+ })])) return grew();
4214
+ return grew();
4167
4215
  }
4168
4216
  /**
4169
- * 提问一轮:followup → 等 idle → 读答案 + 轨迹。
4170
- * @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
4171
- * v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
4172
- * @param options.includeCognitiveTrajectory 是否计算轨迹(v1.26.10 起供**对外面**传 false 跳过计算——
4173
- * 原消费方 = 3100 公开问答页 / ACP JSON-RPC,**该面已于 2026-10-15 退役** ⇒ 现无生产调用方传 false;
4174
- * 3099 调试页默认 true 保留)
4217
+ * 提问一轮:followup → 等**本轮**回答 → 读答案。
4218
+ *
4219
+ * @param options.timeoutMs 本轮等待上限(测试注入;生产用缺省)
4175
4220
  */
4176
- async function askSkiff(ctx, agent, question, eventsStart, options) {
4177
- const before = eventsStart === void 0 ? sessionEvents(agent.session).length : eventsStart;
4221
+ async function askSkiff(ctx, agent, question, options) {
4222
+ const beforeAssistant = countAssistantMessages(agent.session);
4178
4223
  agent.followup(createUserMessage({
4179
4224
  content: [{
4180
4225
  type: "text",
@@ -4182,49 +4227,46 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
4182
4227
  }],
4183
4228
  source: PLUGIN_SOURCE
4184
4229
  }));
4185
- await waitAgentIdle(ctx, agent);
4186
- const answer = lastAssistantText$1(agent);
4187
- const cognitiveTrajectory = options?.includeCognitiveTrajectory === false ? [] : eventsToCognitiveTrajectory(sessionEvents(agent.session).slice(before));
4188
4230
  return {
4189
- answer,
4190
- sessionId: String(agent.session.id ?? ""),
4191
- cognitiveTrajectory
4231
+ answer: await waitForTurnAssistant(ctx, agent, beforeAssistant, { timeoutMs: options?.timeoutMs }) ? lastAssistantText$1(agent) : "",
4232
+ sessionId: String(agent.session.id ?? "")
4192
4233
  };
4193
4234
  }
4194
4235
  /**
4195
- * Skiff 会话的轨迹纪律参与判定:非 skiff 会话恒 true(正常参与);
4196
- * skiff 会话按角色 cognitiveTrajectory 子集(session/keeper/rebuild)决定;
4197
- * 注册表缺失(进程重启遗留等)→ 保守旁路(false,完全独立)。
4236
+ * skiff 会话的轨迹纪律参与判定:**不是 skiff 恒 true**(正常参与);
4237
+ * skiff 按 `skiff.json` 的 `cognitiveTrajectory` 子集决定。
4238
+ *
4239
+ * 注册表缺失(进程刚重启、绑定还没读)⇒ 由 `skiffProfileForSession` 回落持久绑定;
4240
+ * 两者都不中 ⇒ 判"不是 skiff" ⇒ 恒 true(正常参与)。**fail-open 是有意的**:
4241
+ * 纪律参与项的失败不该让会话失去 keeper/rebuild(那是能力倒退,比多跑一次更糟)。
4198
4242
  */
4199
4243
  function skiffCognitiveTrajectoryEnabled(root, sessionId, key) {
4200
- if (!isSkiffSessionId(sessionId)) return true;
4201
- const roleName = sessionId ? skiffRoleFor(sessionId) : null;
4202
- if (!roleName) return false;
4203
- const role = readSkiffRoles(root).get(roleName);
4204
- return cognitiveTrajectorySubset(role)[key];
4244
+ if (sessionId === void 0) return true;
4245
+ const profile = skiffProfileFor(root, sessionId);
4246
+ if (profile === null) return true;
4247
+ return profileCognitiveTrajectory(profile)[key];
4205
4248
  }
4206
4249
  /**
4207
- * msm 工具的 Skiff 门控:非 skiff 会话恒放行;skiff 会话——
4208
- * exec 非白名单 MSM 拒绝(不列名单)、register/deregister 必拒、
4250
+ * `msm` 工具的 skiff 门控:非 skiff 恒放行;skiff 会话——
4251
+ * exec 非白名单 MSM 拒绝(**不列名单**)、register/deregister 必拒、
4209
4252
  * list 白名单过滤、check/guide/ccc-config 只读放行。
4210
4253
  */
4211
4254
  function skiffMsmGate(root, sessionId, action, name) {
4212
- if (!isSkiffSessionId(sessionId)) return {};
4213
- const roleName = sessionId ? skiffRoleFor(sessionId) : null;
4214
- const role = roleName ? readSkiffRoles(root).get(roleName) : void 0;
4215
- if (!roleName || !role) return { reject: "MSM not allowed in this skiff session" };
4255
+ if (sessionId === void 0) return {};
4256
+ const profile = skiffProfileFor(root, sessionId);
4257
+ if (profile === null) return {};
4216
4258
  if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in skiff sessions" };
4217
4259
  if (action === "exec") {
4218
- if (!name || !(role.msms ?? []).includes(name)) return { reject: "MSM not allowed" };
4260
+ if (!name || !profile.msms.includes(name)) return { reject: "MSM not allowed" };
4219
4261
  return {};
4220
4262
  }
4221
- if (action === "list") return { whitelist: roleMsmWhitelist(role) };
4263
+ if (action === "list") return { whitelist: profileMsmWhitelist(profile) };
4222
4264
  return {};
4223
4265
  }
4224
4266
  //#endregion
4225
- //#region src/tools/msm.ts
4267
+ //#region src/tools/msm/index.ts
4226
4268
  /**
4227
- * msm.ts — msm 工具(v1.30 重构:单入口执行,list/exec 合一)
4269
+ * tools/msm/index.ts — msm 工具(v1.30 重构:单入口执行,list/exec 合一)
4228
4270
  *
4229
4271
  * 设计(S142 用户拍板"msm 单入口同意 + 参数简易高效"):
4230
4272
  * acc_msm(8 action enum)→ msm(3 参数:name + args + inspect)——
@@ -4334,7 +4376,7 @@ const msmTool = defineTool({
4334
4376
  * ⇒ 正文**不再内嵌**(58 KB 塞进 TS 模板字符串还要转义论文里 4 处 ```mermaid 围栏——
4335
4377
  * 本容器为此踩过四次),改为**随包的静态资产 + 运行时读取**:
4336
4378
  * 资产 = `assets/eap-paper.md`(**逐字**来自上游论文,未经改写)
4337
- * 读取 = `loadEapPaper()`(进程内缓存一次;同 `voyage-page.ts` 的既有先例)
4379
+ * 读取 = `loadEapPaper()`(进程内缓存一次;同 `surface/voyage/page.ts` 的既有先例)
4338
4380
  *
4339
4381
  * 🔴 **单一真相源**:正文只在 `assets/eap-paper.md` 一个地方。本文件负责**定位与读取**,
4340
4382
  * `praxis.ts` 只负责**路由**。二者都不得内嵌任何论文片段——否则又是"第二份会漂的正文"。
@@ -8282,7 +8324,7 @@ const DIAGRAM_FONT_STACK = `'Noto Sans', 'DejaVu Sans', '${DIAGRAM_CJK_FONT_FAMI
8282
8324
  /**
8283
8325
  * 包根 = **向上找到含 `package.json` 的那一级**。
8284
8326
  *
8285
- * 🔴 为什么不能照抄 `voyage-page.ts` 的「`..` 一次」:那个文件在 `src/` 根下,一次 `..` 恰好是包根;
8327
+ * 🔴 为什么不能照抄 `surface/voyage/page.ts` 的「`..` 一次」:那个文件在 `src/` 根下,一次 `..` 恰好是包根;
8286
8328
  * 本模块在 `src/diagram/` 里 ⇒ 开发态(vitest 直接跑 TS 源码)一次 `..` 只到 `src/`,
8287
8329
  * 于是去找 `src/assets/…` —— **实测 ENOENT**(测试全红,且现象是"字体不在",与真因"路径层级算错"
8288
8330
  * 长得完全不同)。按 `package.json` 上溯对**开发态(src/diagram)与发布态(lib/diagram)都成立**。
@@ -9436,9 +9478,9 @@ function createCheapWorkerTool(ctx) {
9436
9478
  });
9437
9479
  }
9438
9480
  //#endregion
9439
- //#region src/session-cleanup.ts
9481
+ //#region src/trajectory/cleanup.ts
9440
9482
  /**
9441
- * session-cleanup.ts — DSH 平台旧会话自动清理(v1.29,S142 需求③)
9483
+ * trajectory/cleanup.ts — DSH 平台旧会话自动清理(v1.29,S142 需求③)
9442
9484
  *
9443
9485
  * 背景(实证):DSH 会话物理存储 = `$DSH_HOME/sessions/`(缺省 ~/.dsh/sessions;
9444
9486
  * base bundle cordis.patch.yml root: dshHomePath('sessions'))。磁盘布局:
@@ -9455,7 +9497,7 @@ function createCheapWorkerTool(ctx) {
9455
9497
  *
9456
9498
  * 本模块纯逻辑(零 ctx 依赖):传入 sessionsRoot + liveIds,可单测。
9457
9499
  */
9458
- var session_cleanup_exports = /* @__PURE__ */ __exportAll({
9500
+ var cleanup_exports = /* @__PURE__ */ __exportAll({
9459
9501
  collectEligibleSessions: () => collectEligibleSessions,
9460
9502
  cutoffDaysAgo: () => cutoffDaysAgo,
9461
9503
  findSessionLog: () => findSessionLog,
@@ -9645,9 +9687,9 @@ function cutoffDaysAgo(days, nowMs = Date.now()) {
9645
9687
  return nowMs - days * DAY_MS;
9646
9688
  }
9647
9689
  //#endregion
9648
- //#region src/wake-registry.ts
9690
+ //#region src/wake/registry.ts
9649
9691
  /**
9650
- * wake-registry.ts — 认知轨迹唤醒注册表(D58,S142 2026-09-13)
9692
+ * wake/registry.ts — 认知轨迹唤醒注册表(D58,S142 2026-09-13)
9651
9693
  *
9652
9694
  * 需求(用户原话):一个 CCC 内允许多个认知轨迹任意形态并行;某个认知轨迹可以
9653
9695
  * **预定未来唤醒自己或别人**,本质上是一次「未来时刻 + 一条 message」的投递;「周期自唤醒」
@@ -9659,7 +9701,7 @@ function cutoffDaysAgo(days, nowMs = Date.now()) {
9659
9701
  * - 状态机:`pending` →(投递成功)`delivered`|(超补跑窗口)`missed`|(`wake rm` 物理移除)
9660
9702
  * - 投递即终结:一次性,不重投(无回执语义——发起方不 await,见设计文档 §2)
9661
9703
  *
9662
- * 调度与投递在 `wake-scheduler.ts`(需要 ctx),本模块保持纯净。
9704
+ * 调度与投递在 `wake/scheduler.ts`(需要 ctx),本模块保持纯净。
9663
9705
  */
9664
9706
  /** 注册表相对 CCC 根的路径(与 `.bindings.json` 同居 AGENT_SESSIONS/) */
9665
9707
  const WAKE_REGISTRY_REL_PATH = "AGENT_SESSIONS/wake-registry.json";
@@ -10001,9 +10043,9 @@ function listWakes(root, opts = {}) {
10001
10043
  };
10002
10044
  }
10003
10045
  //#endregion
10004
- //#region src/cro.ts
10046
+ //#region src/wake/cro.ts
10005
10047
  /**
10006
- * cro.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
10048
+ * wake/cro.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
10007
10049
  *
10008
10050
  * ## 这东西用来干什么(白话)
10009
10051
  *
@@ -10044,7 +10086,7 @@ function listWakes(root, opts = {}) {
10044
10086
  * ## 🔴 铁律:CRO 的任何失败,绝不影响既有机制(设计 §5)
10045
10087
  *
10046
10088
  * `send-later` / `send-now` / 唤醒表的投递**照常工作**。
10047
- * 先例 = `weixin-hook.ts` 的**旁路容忍**("超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回,绝不抛")。
10089
+ * 先例 = `im/hook.ts` 的**旁路容忍**("超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回,绝不抛")。
10048
10090
  * ⇒ 本模块的**每一个**导出函数**都不抛错**(返回结构化失败)。
10049
10091
  */
10050
10092
  /** 入口文件名(**全写**,所有者令 2026-09-19;不改缩写) */
@@ -10063,7 +10105,7 @@ const CRO_TIMEOUT_MS = 6e4;
10063
10105
  const CRO_STDOUT_MAX = 65536;
10064
10106
  /** 绑定文件体积上限(它只是个机器名 ⇒ 超过这个数一定是写错了,不读) */
10065
10107
  const CRO_HOST_FILE_MAX = 1024;
10066
- /** SESSION.md 文件名(与 `cognitive-trajectory-ops.ts` 同值;此处本地常量避免反向依赖) */
10108
+ /** SESSION.md 文件名(与 `trajectory/core.ts` 同值;此处本地常量避免反向依赖) */
10067
10109
  const SESSION_MD = "SESSION.md";
10068
10110
  /**
10069
10111
  * 展示码派生(**只用于展示**,不作识别依据 —— §0I U4:锚永远是 `dirName`)。
@@ -10188,7 +10230,7 @@ function parseCroOutput(stdout) {
10188
10230
  * CRO 程序路径:`<CCC 根>/AGENT_SESSIONS/<dirName>/continuous-re-occurrence.ts`。
10189
10231
  *
10190
10232
  * ⚠️ 路径逃逸校验(`resolveInside`):`dirName` 来自配置/工具入参,
10191
- * 必须确保解析后仍在 CCC 根内(同 `weixin-hook.ts:160` 手法)。**抛错由调用方吞**。
10233
+ * 必须确保解析后仍在 CCC 根内(同 `im/hook.ts:160` 手法)。**抛错由调用方吞**。
10192
10234
  * @param root CCC 根
10193
10235
  * @param dirName 轨迹目录名
10194
10236
  * @returns 绝对路径
@@ -10365,7 +10407,7 @@ function listCroCognitiveTrajectories(root) {
10365
10407
  /**
10366
10408
  * 执行一次 CRO 程序(**永不抛** —— 旁路容忍铁律)。
10367
10409
  *
10368
- * runner 顺序(照 `weixin-hook.ts:171-174` 的 bun 优先 / node 兜底):
10410
+ * runner 顺序(照 `im/hook.ts:171-174` 的 bun 优先 / node 兜底):
10369
10411
  * `bun` → `process.execPath`(同运行时)。
10370
10412
  * 判据:`ENOENT`(二进制不存在)⇒ 试下一个;**其余失败视为最终结果**
10371
10413
  * (程序自己报错就是报错,不该用另一个 runner 掩盖——与 hook 一致)。
@@ -10627,9 +10669,9 @@ function registerDisposer(ctx, label, cleanup) {
10627
10669
  }
10628
10670
  }
10629
10671
  //#endregion
10630
- //#region src/cro-turns.ts
10672
+ //#region src/wake/cro-turns.ts
10631
10673
  /**
10632
- * cro-turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
10674
+ * wake/cro-turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
10633
10675
  *
10634
10676
  * ## 为什么需要这个模块(CRO 唯一需要 ACC 新增的状态)
10635
10677
  *
@@ -10643,7 +10685,7 @@ function registerDisposer(ctx, label, cleanup) {
10643
10685
  * | 事件 | 动作 | 出处 |
10644
10686
  * |---|---|---|
10645
10687
  * | `agent/created` | 标记「在跑」 | `host/contract.ts`(dsp 已在 `seams/context.ts` 订阅)。⚠️ v0.1.7 前名为 `agent/session-start`;新事件 `@mode = serial` ⇒ 处理函数须快速返回 |
10646
- * | `agent/turn-stopping` | 清除标记 | `host/contract.ts:277`(dsp 已在 `rebuild.ts:442` 等 4 处订阅) |
10688
+ * | `agent/turn-stopping` | 清除标记 | `host/contract.ts:277`(dsp 已在 `trajectory/rebuild.ts:442` 等 4 处订阅) |
10647
10689
  * | `agent/disposed` | **清该载体的项** | `host/contract.ts:280` |
10648
10690
  * | `session/disposed` | **清该载体的项** | `host/contract.ts:283` |
10649
10691
  *
@@ -10755,13 +10797,13 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
10755
10797
  * 🔴 **本处顺带更正 §2.7 的一处分档**:本文件原被列在"剩余 3 个属**真依赖**档"里,
10756
10798
  * 但**逐符号查证后与那个判断不符** ——
10757
10799
  * · `Context`:`ctx` 只被**结构化访问**(原码本就写着 `ctx as unknown as { on: … }`)
10758
- * ⇒ 该注解未提供任何类型保证(与 `agent-idle.ts` 同档)。
10800
+ * ⇒ 该注解未提供任何类型保证(与 `role/worker/idle.ts` 同档)。
10759
10801
  * · `Agent`:**导入后从未使用** —— 事件 payload 一律按 `{ agent?: unknown }` 解构,
10760
10802
  * 再交给 `sessionIdOf(...)`(它收 `unknown`)。⇒ **纯粹是死 import**。
10761
10803
  * ⇒ **判据(本件再证一次,且比上一件更硬)**:分档**必须逐符号看"它有没有参与类型检查"**,
10762
10804
  * **不能按文件整档划分** —— 同一个文件里可以并存"真依赖符号"与"纯名义符号"。
10763
- * 🔵 **剩余 2 个文件**才是真依赖档:`rebuild.ts` 用 `Session`/`SessionEvent`/
10764
- * `deriveEventMessage`/`Agent` 等**真调用**的类型;`wake-scheduler.ts` 用 `Agent` 的
10805
+ * 🔵 **剩余 2 个文件**才是真依赖档:`trajectory/rebuild.ts` 用 `Session`/`SessionEvent`/
10806
+ * `deriveEventMessage`/`Agent` 等**真调用**的类型;`wake/scheduler.ts` 用 `Agent` 的
10765
10807
  * `followup`/`steer`(**真调用**)⇒ 它们走 §2.7-③ **选项 (b)** 或把中立接口下沉到 L0,
10766
10808
  * **另立一件**(D85 一次只改一件事)。
10767
10809
  * @param ctx 插件上下文(**结构化访问** `on`,故类型为 `unknown`,见下方 I2 说明)
@@ -10803,9 +10845,9 @@ function registerCroTurnTracking(ctx) {
10803
10845
  });
10804
10846
  }
10805
10847
  //#endregion
10806
- //#region src/cro-log.ts
10848
+ //#region src/wake/cro-log.ts
10807
10849
  /**
10808
- * cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
10850
+ * wake/cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
10809
10851
  *
10810
10852
  * ## 所有者令(本模块的存在理由)
10811
10853
  *
@@ -10837,11 +10879,11 @@ function registerCroTurnTracking(ctx) {
10837
10879
  * ## 🔴 轮转(所有者明示「程序化的」)
10838
10880
  *
10839
10881
  * **写入时按窗裁剪**:每次落盘只保留 `at >= now - CRO_LOG_RETENTION_MS` 的条目,
10840
- * 随后**原子写**(`tmp + rename`,与 `wake-registry.ts` 同款形态)。
10882
+ * 随后**原子写**(`tmp + rename`,与 `wake/registry.ts` 同款形态)。
10841
10883
  * ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
10842
10884
  * 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
10843
10885
  *
10844
- * ## 🔴 铁律(与 `cro.ts` 同族)
10886
+ * ## 🔴 铁律(与 `wake/cro.ts` 同族)
10845
10887
  *
10846
10888
  * **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
10847
10889
  * 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
@@ -11007,12 +11049,12 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
11007
11049
  }
11008
11050
  }
11009
11051
  //#endregion
11010
- //#region src/clock-runtime.ts
11052
+ //#region src/wake/clock.ts
11011
11053
  /**
11012
- * clock-runtime.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
11054
+ * wake/clock.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
11013
11055
  *
11014
11056
  * 🔴 2026-09-25(S142 分批清死代码·第 1 批)**更正**:本文件当初是为**两条**独立 5min 时钟抽出的公共引擎:
11015
- * · 唤醒调度器(`wake-scheduler.ts`,一次性唤醒,D58)—— **现存**
11057
+ * · 唤醒调度器(`wake/scheduler.ts`,一次性唤醒,D58)—— **现存**
11016
11058
  * · autopilot 时钟(`autopilot-trajectory.ts`,周期自唤醒,D59)—— 🔴 **已随 v1.35.0 整段退场**
11017
11059
  * 二者的**业务判据完全不同**(谁来扫 / 扫什么 / 投递给谁 / 按什么周期),但**引擎骨架逐字同形**
11018
11060
  * ——进程态 + 快照 + 复位 + `startTimer` + tick 外壳 + 事件接线 + disposer + `timer.unref()`。
@@ -11059,7 +11101,7 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
11059
11101
  * **本工厂是"加法式"重构**(P2 的性质):不新增任何判定逻辑,只把同形骨架收成一份代码;
11060
11102
  * 两个模块仍各自导出 `wakeSchedulerState()` / `autopilotClockState()` 与各自的
11061
11103
  * `__reset…ForTest()`,**对外可观测面(字段名与语义)零变化**——`acc-diag` ①b 段、
11062
- * `container-status.ts:containerClocks()` 与面板都在读它们(见该文件 :225–250 的设计红线)。
11104
+ * `surface/observation.ts:containerClocks()` 与面板都在读它们(见该文件 :225–250 的设计红线)。
11063
11105
  *
11064
11106
  * 为什么**不**取 P1(真合并成一个 `setInterval` + 两条策略)——2026-09-15 所有者裁决:
11065
11107
  * P1 只多省 ~20 行,却要把 `armed` 拆成"宿主 + 每策略"两层、并强行分开串行域,
@@ -11215,9 +11257,9 @@ function createClock(opts) {
11215
11257
  return api;
11216
11258
  }
11217
11259
  //#endregion
11218
- //#region src/wake-scheduler.ts
11260
+ //#region src/wake/scheduler.ts
11219
11261
  /**
11220
- * wake-scheduler.ts — 认知轨迹唤醒调度器(D58/D59/D60;S142 2026-09-13)
11262
+ * wake/scheduler.ts — 认知轨迹唤醒调度器(D58/D59/D60;S142 2026-09-13)
11221
11263
  *
11222
11264
  * 一个**中心调度器**(5min tick,复用 autopilot 的节律常量),处理 CCC 唤醒注册表里
11223
11265
  * 到期的条目:把「未来时刻 + 一条 message」投递给目标认知轨迹,**fire-and-forget**
@@ -11964,9 +12006,9 @@ function registerWakeScheduler(ctx) {
11964
12006
  registerDisposer(ctx, "认知轨迹唤醒调度器", () => clock.dispose());
11965
12007
  }
11966
12008
  //#endregion
11967
- //#region src/cro-guide.ts
12009
+ //#region src/wake/cro-guide.ts
11968
12010
  /**
11969
- * cro-guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
12011
+ * wake/cro-guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
11970
12012
  *
11971
12013
  * ## 为什么指南在 ACC 而不在 CCC(所有者令)
11972
12014
  *
@@ -12212,7 +12254,7 @@ ${renderCroSampleSnapshot()}
12212
12254
  * 动作面现行 **8 个**:list / show / create / use / rebuild / **`send-now`** / **`send-later`** / **`cro-guide`**。
12213
12255
  * 🔴 2026-09-16(所有者裁决):**第 7 个动作 `send-message`(即时投递)**
12214
12256
  * ——与 `wake-later` 共用取用通路,差别只在**时刻**(现在 vs 未来)与**回执**(有 vs 无);
12215
- * 实现落在 `wake-scheduler.ts` 的 `sendToTrajectory()`("怎么把一句话送到一条轨迹"是调度器的知识)。
12257
+ * 实现落在 `wake/scheduler.ts` 的 `sendToTrajectory()`("怎么把一句话送到一条轨迹"是调度器的知识)。
12216
12258
  * 设计稿 = `docs/cognitive-trajectory-send-message-design.md`(v0.2 定稿)。
12217
12259
  * 🔴 2026-09-17(所有者裁决 · **命名 A 案**):**两个投递动作更名** —— `wake-later` → **`send-later`**、
12218
12260
  * `send-message` → **`send-now`**。判据(R↓):两者本是**同一条投递通路**(同取用机制 / 同载荷 /
@@ -12350,6 +12392,8 @@ function createCognitiveTrajectoryTool(ctx) {
12350
12392
  "cognitive_trajectory — one cognitive trajectory: its persistent body (SESSION.md) + its timeline.",
12351
12393
  "A cognitive trajectory is NOT the conversation: SESSION.md is the durable body and never moves;",
12352
12394
  "the dsh conversation is only its rebuildable carrier (Ship of Theseus — the carrier is replaced, the cognitive trajectory continues).",
12395
+ "IDENTITY: every trajectory has a CODE — the fixed-position segment of its directory name (YYYY-MM-DD--<code>[--<desc>]).",
12396
+ "The code IS the trajectory's unique identifier, and you can choose it at creation time with create --id.",
12353
12397
  "A COGNITIVE TRAJECTORY HAS NO LIFECYCLE — it does not complete, close, or expire, and age never counts against it;",
12354
12398
  "it PERMANENTLY EXISTS. The only thing that ever changes is WHERE it is stored: archive moves it out of the active list",
12355
12399
  "without touching a single byte of its directory or SESSION.md. There is no reverse action — to restore one, use container_fs mv.",
@@ -12376,12 +12420,13 @@ function createCognitiveTrajectoryTool(ctx) {
12376
12420
  "ASYMMETRY (deliberate — the two differ in whether a missing target is normal): send-now creates a target on demand,",
12377
12421
  "because sending to a never-used cognitive trajectory is a legitimate intent; send-later does NOT,",
12378
12422
  "because scheduling for a cognitive trajectory that has never existed is almost always a mistyped name — auto-creating would hide that error behind an empty cognitive trajectory.",
12379
- "PARAMETERS: create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars).",
12423
+ "PARAMETERS: create requires --desc <desc> [--id <code>] [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars).",
12380
12424
  "use requires --summary (≤20 chars) and may pass --force to switch away from the currently-bound cognitive trajectory.",
12381
12425
  "rebuild requires --summary (next-phase summary ≤20 chars) + optional --note (task focus for the rebuilt self).",
12382
- "send-later requires target (S### or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message.",
12426
+ "send-later requires target (a code or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message.",
12383
12427
  "send-now requires target + message.",
12384
- "The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the S### id and date stay server-derived."
12428
+ "uid: READS a legacy TRAJECTORY-UID only — setting one is retired (codes replaced it). Use create --id instead.",
12429
+ "The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the code and date stay server-derived unless you pass --id."
12385
12430
  ].join(" "),
12386
12431
  parameters: {
12387
12432
  action: {
@@ -12391,8 +12436,8 @@ function createCognitiveTrajectoryTool(ctx) {
12391
12436
  description: [
12392
12437
  "Subcommand — grouped by effect.",
12393
12438
  "LIFECYCLE (changes THIS conversation's binding):",
12394
- "list (inventory + stats + anomaly marks) / show (S### or dir name or fuzzy keyword) /",
12395
- "create (new cognitive trajectory; activates itself when you have no binding, otherwise leaves your binding untouched and tells you how to activate it) /",
12439
+ "list (inventory + stats + anomaly marks) / show (a code, dir name, or fuzzy keyword) /",
12440
+ "create (new cognitive trajectory; --id sets its code. Activates itself when you have no binding, otherwise leaves your binding untouched and tells you how to activate it) /",
12396
12441
  "use (activate context for THIS conversation; inline integrity check — pass silent, problems reported as hints, not errors) /",
12397
12442
  "rebuild (clear-and-rebuild current conversation — requires --summary + optional --note) /",
12398
12443
  "archive (move a trajectory to the archived area; one-way, refuses to overwrite, content untouched — --dryRun previews).",
@@ -12404,11 +12449,11 @@ function createCognitiveTrajectoryTool(ctx) {
12404
12449
  },
12405
12450
  name: {
12406
12451
  type: "string",
12407
- description: "show/use/archive/uid identifier — S### / dir name / UID / fuzzy keyword"
12452
+ description: "show/use/archive/uid identifier — a trajectory code (S### or a custom code) / dir name / legacy UID / fuzzy keyword"
12408
12453
  },
12409
12454
  uid: {
12410
12455
  type: "string",
12411
- description: `uid: the trajectory's unique identifier (a separate, optional handle independent of its code and name — file ${UID_FILENAME} inside the trajectory dir). Omit to READ it; pass a value to SET it; pass "" to CLEAR it (falls back to the code). Allowed [A-Za-z0-9._-], ≤64, case-sensitive, must be unique across trajectories.`
12456
+ description: `uid: READ-ONLY legacy field. Omit to read a trajectory's legacy ${UID_FILENAME}. Passing any value is REJECTED — setting a UID is retired (2026-10-19); a trajectory's code is its unique identity now, and you choose it at creation with create --id <code>.`
12412
12457
  },
12413
12458
  note: {
12414
12459
  type: "string",
@@ -12418,6 +12463,10 @@ function createCognitiveTrajectoryTool(ctx) {
12418
12463
  type: "string",
12419
12464
  description: "create: short description (any language, ≤5 words; mutually exclusive with issue)"
12420
12465
  },
12466
+ id: {
12467
+ type: "string",
12468
+ description: "create: the trajectory CODE to assign (its stable unique identity — this is what you later pass as name/target). Omit to auto-assign S###. Allowed [A-Za-z0-9._-], starts alphanumeric, ≤64, must NOT contain Chinese or \"--\" (that is the dir-name separator); must not collide with an active trajectory code."
12469
+ },
12421
12470
  issue: {
12422
12471
  type: "string",
12423
12472
  description: "create: ticket number (e.g. apaas-26116; dir named YYYY-MM-DD--<issue>; mutually exclusive with desc)"
@@ -12471,6 +12520,7 @@ function createCognitiveTrajectoryTool(ctx) {
12471
12520
  root,
12472
12521
  desc: args.desc,
12473
12522
  issue: args.issue,
12523
+ id: args.id,
12474
12524
  goal: args.goal,
12475
12525
  dryRun: isDryRun
12476
12526
  });
@@ -12572,26 +12622,18 @@ function createCognitiveTrajectoryTool(ctx) {
12572
12622
  };
12573
12623
  }
12574
12624
  case "uid": {
12575
- if (!args.name) throw new Error("uid requires name (S### / dir name / existing UID)");
12576
- if (args.uid === void 0) {
12577
- const entry = findSession(join(root, "AGENT_SESSIONS"), args.name);
12578
- if (!entry) throw new Error(`Trajectory not found: "${args.name}"`);
12579
- const cur = readTrajectoryUid(entry.path);
12580
- return {
12581
- ok: true,
12582
- dirName: entry.dirName,
12583
- uid: cur,
12584
- note: cur === null ? `未设定 UID ⇒ 用编码指认(${entry.dirName.match(/--(S\d{3,})--/)?.[1] ?? "无编码"})。` : `当前 UID = ${cur}(文件 ${UID_FILENAME})`
12585
- };
12586
- }
12587
- const next = args.uid === "" ? null : String(args.uid);
12588
- const r = setTrajectoryUid(join(root, "AGENT_SESSIONS"), args.name, next);
12625
+ if (!args.name) throw new Error("uid requires name (code / S### / dir name / existing UID)");
12626
+ const entry = findSession(join(root, "AGENT_SESSIONS"), args.name);
12627
+ if (!entry) throw new Error(`Trajectory not found: "${args.name}"`);
12628
+ const cur = readTrajectoryUid(entry.path);
12629
+ if (args.uid !== void 0) throw new Error("Setting a trajectory UID has been retired (2026-10-19). A trajectory's code IS its unique identity now, and you can choose it at creation time: cognitive_trajectory create --id <code> --desc <desc> --summary <summary>. Existing TRAJECTORY-UID files are still READ (so old handles keep resolving) — they are just never written.");
12589
12630
  return {
12590
12631
  ok: true,
12591
- dirName: r.dirName,
12592
- uid: r.uid,
12593
- message: r.uid === null ? `已清除 UID ⇒ "${r.dirName}" 回到用编码指认` : `已设定 UID = "${r.uid}" ⇒ 所有支持编码的动作(show/use/archive/send-now/send-later/uid)现在也接受它`,
12594
- path: r.path
12632
+ dirName: entry.dirName,
12633
+ code: parseTrajectoryCode(entry.dirName),
12634
+ uid: cur,
12635
+ retired: true,
12636
+ note: cur === null ? `未设定遗留 UID ⇒ 用编码指认(${parseTrajectoryCode(entry.dirName) ?? "无编码"})。UID 写入已退役;要一条可指定的标识,请用 create --id 起新轨迹。` : `遗留 UID = ${cur}(文件 ${UID_FILENAME},只读兼容)——仍可用它指认本条轨迹。⚠️ 写入已退役:新轨迹用 create --id 指定编码。`
12595
12637
  };
12596
12638
  }
12597
12639
  case "send-later": {
@@ -12672,12 +12714,12 @@ function advisoryHint(dirName, mdPath) {
12672
12714
  return null;
12673
12715
  }
12674
12716
  //#endregion
12675
- //#region src/tools/localstore.ts
12717
+ //#region src/tools/localstore/index.ts
12676
12718
  /**
12677
12719
  * localstore.ts — localstore 真实 DSH 工具定义(defineTool)
12678
12720
  *
12679
12721
  * ACC 标准本地凭据/配置存储(S133 设计,S134 重设计:CCC 根 localstore.json)。
12680
- * 进程内注册,零 DSH 依赖逻辑在 localstore-ops.ts(可单测)。
12722
+ * 进程内注册,零 DSH 依赖逻辑在 tools/localstore/service.ts(可单测)。
12681
12723
  * doc 子命令输出存储规范,agent 可直接用 fs 工具(read/write)自己读写。
12682
12724
  */
12683
12725
  const ACTIONS$1 = [
@@ -12730,106 +12772,96 @@ const localstoreTool = defineTool({
12730
12772
  });
12731
12773
  //#endregion
12732
12774
  //#region src/tools/skiff-admin.ts
12733
- /** Skiff 定义教程(核心):概念 / schema / 认知 MSM 写法 / 双白名单 / 轨迹纪律 / 示例角色 */
12734
- const SKIFF_GUIDE = `═══ Skiff Definition Guide (F4, experimental) ═══
12775
+ /** Skiff 定义教程:概念 / `skiff.json` schema / 双白名单 / 轨迹纪律 / 微信路由 */
12776
+ const SKIFF_GUIDE = `═══ Skiff Guide (2026-10-19 redesigned) ═══
12735
12777
 
12736
- Skiff = 宁静号放出的独立小艇——完整 cognitive trajectory(在宁静号内全知全能)的**任意子集角色**。
12737
- 由本 CCC(.opencode/serenity.json skiff.roles)定义:能力面(双白名单)+ 轨迹纪律子集 + 系统提示词。
12738
- dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页);角色内容完全由 CCC 发挥。
12778
+ **A skiff IS a cognitive trajectory — the one whose directory contains \`${SKIFF_PROFILE_FILENAME}\`.**
12779
+ That is the whole rule. No role table, no name registry: put the file there and the trajectory
12780
+ becomes a skiff; delete it and it goes back to being an ordinary trajectory (takes effect at once).
12739
12781
 
12740
- ── 角色配置 schema ──
12741
- { "skiff": { "roles": {
12742
- "<role-name>": {
12743
- "model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 cheap-worker.defaultModel)
12744
- "msms": ["msm-a", "msm-b"], // MSM 白名单(独立):msm 工具只能执行这些;register/deregister 必拒
12745
- "tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
12746
- "cognitiveTrajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
12747
- // 角色人格/认知边界/风格(CCC 完整定义;dsp 只给基础提示词)
12748
- "systemPromptFile": ".opencode/skiff/<role>.md" // ★ 推荐:引用 md 文件(相对 CCC 根;超长提示词在 JSON 内嵌不可读)
12749
- // "systemPrompt": "..." // 兼容:内嵌字符串仍可用;两者都设时 systemPromptFile 优先
12750
- }
12751
- } } }
12782
+ AGENT_SESSIONS/<YYYY-MM-DD--<code>--<desc>>/
12783
+ ├─ SESSION.md ← its persistent body (this IS the workspace — nothing is auto-created)
12784
+ ├─ ${SKIFF_PROFILE_FILENAME} ← its constraints (this file is what makes it a skiff)
12785
+ └─ TRAJECTORY-UID ← legacy, read-only (superseded by the code)
12752
12786
 
12753
- ── 双白名单语义(全按白名单暴露,白名单外全隐藏)──
12754
- - tools 空 + msms 非空 = 纯 MSM 角色(认知问答典型形态)
12755
- - msms 非空 → msm 工具自动可用(MSM 通道)
12756
- - 白名单外工具即使 DSH 未来新增也自动被挡(guard 按角色判定,不枚举工具名——完备性)
12787
+ ── ${SKIFF_PROFILE_FILENAME} schema ──
12788
+ {
12789
+ "model": "provider/model", // optional; falls back to cheap-worker.defaultModel
12790
+ "tools": ["read","grep","msm",...], // non-MSM toolbar whitelist — anything not listed is REMOVED from the schema
12791
+ "msms": ["msm-a","msm-b"], // MSM whitelist — msm exec may only run these; register/deregister always refused
12792
+ "cognitiveTrajectory": { "session": true, "keeper": true, "rebuild": true }, // defaults TRUE (see below)
12793
+ "systemPromptFile": ".opencode/skiff/<role>.md", // recommended: relative to CCC root, hot-reloads on save
12794
+ "systemPrompt": "..." // inline fallback; systemPromptFile wins when both are set
12795
+ }
12757
12796
 
12758
- ── 认知 MSM 写法(读知识 / 操作能力)──
12759
- - 读知识:脚本读 CCC 内文件(SERENITY_ROOT env 注入)→ 输出答案/摘要(如 cognitive-qa)
12760
- - 操作能力:脚本调用既有家庭工具/服务(SSH/API/文件操作)——角色能力上限 = 白名单 MSM 的实际行为
12761
- - 例:cognitive-qa(读 docs/references 回答)、review-scan(读代码出审查意见)、review-fix(写修复)
12762
- - 注意:MSM 在脚本层执行(bun),文件访问不受 agent 工具面约束——"只读"语义靠 CCC 自写 MSM 自觉
12797
+ ── Two whitelists, both default-deny ──
12798
+ - \`tools: []\` + non-empty \`msms\` = pure-MSM skiff (the usual knowledge-QA shape)
12799
+ - non-empty \`msms\` ⇒ the \`msm\` tool is added automatically (the only entry point for MSMs)
12800
+ - anything outside both lists is invisible in the schema, not merely rejected at call time
12763
12801
 
12764
- ── 轨迹纪律子集(cognitiveTrajectory)──
12765
- - session/keeper/rebuild 默认 false:Skiff 完全独立(不建 SESSION.md、无 keeper 提醒、无 rebuild 压力检测)
12766
- - 开启某项 = 该角色参与对应轨迹机制(如 keeper=true 计分提醒按角色生效)
12802
+ ── cognitiveTrajectory: why the defaults are TRUE ──
12803
+ The workspace of a skiff is its own SESSION.md. Setting these to false would cut it off from
12804
+ its own body, so \`session\`/\`keeper\`/\`rebuild\` all default to true. Set false explicitly only
12805
+ when you deliberately want that participation off.
12767
12806
 
12768
- ── 示例角色 ──
12769
- qa-readonly: { "msms": ["cognitive-qa"], "tools": [], "cognitiveTrajectory": {} }
12770
- → 认知问答(仅 MSM 通道,无直接工具,完全独立)
12771
- code-review: { "msms": ["review-scan","review-fix"], "tools": ["read","grep","glob","write","edit"], "cognitiveTrajectory": { "keeper": true } }
12772
- → 有操作能力(可写修复),参与 keeper 轨迹机制
12807
+ ── WeChat routing: user → trajectory code ──
12808
+ \`weixin.routes\`: \`[{ "user": "<wx id or *>", "trajectory": "<code>" }]\`
12809
+ A route pointing at a code with no such trajectory is a hard error (the message is NOT delivered).
12810
+ A trajectory WITHOUT \`${SKIFF_PROFILE_FILENAME}\` is still a legal target — it is just an ordinary trajectory.
12773
12811
 
12774
- ── 运行 ──
12775
- 调试:设置面板「Serenity」页 Skiff 区块开启 → http://127.0.0.1:<debugPort> 问答页实测
12776
- 🔴 外部程序化接入(2026-10-15 起改走官方):本插件自造的 ACP 面(HTTP JSON-RPC 3100 /
12777
- 建议问答页)**已整体退役**(owner 裁「砍掉外部问答」)。现在要程序化/远程接入,走宿主
12778
- 自带的官方 ACP server:dsh --profile acp(纯 stdio、零认证)+ 任一 ACP 客户端
12779
- (手机端如 Agmente / ACP UI)经隧道接入。详见 S142 的接入指南文档。
12780
- 启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用
12781
- 生效机制:角色配置**实时读取**(改 .opencode/serenity.json → 刷新调试页即生效);
12782
- 改配置后用 container_admin role apply 做显式校验 + 应用确认(绑定 CCC + 角色清单)
12783
- 会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
12784
- 「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
12785
- 注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
12786
- /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ cheap-worker.models / systemPrompt 非空 */
12812
+ ── Operating it ──
12813
+ Debug page: Settings → Serenity → enable the Skiff block → http://127.0.0.1:<debugPort>
12814
+ Everything is read live: saving \`${SKIFF_PROFILE_FILENAME}\` takes effect on the next turn, no restart.
12815
+ (The prompt FILE hot-reloads by mtime; the JSON itself is re-read every turn.)
12816
+ After editing, run container_admin role apply for an explicit validation + confirmation pass.
12817
+ Skill loading is always available to skiff sessions (skills are not whitelisted — they are read-only knowledge).`;
12818
+ /** 校验当前 CCC 的 skiff 约束面:msms 均已注册 / model ∈ cheap-worker.models / 提示词非空 */
12787
12819
  function validateSkiffConfig(root) {
12788
- const roles = readSkiffRoles(root);
12820
+ const profiles = listSkiffProfiles(root);
12789
12821
  const issues = [];
12790
- if (roles.size === 0) return {
12822
+ if (profiles.length === 0) return {
12791
12823
  ok: true,
12792
12824
  issues: [],
12793
12825
  roleCount: 0,
12794
- note: "no skiff roles defined (skiff disabled — zero impact)"
12826
+ note: `no skiff trajectories in this CCC (no trajectory has ${SKIFF_PROFILE_FILENAME} — zero impact)`
12795
12827
  };
12796
12828
  const registered = new Set(loadMsmEntries(root).map((e) => e.name));
12797
12829
  const hc = readCheapWorkerConfig(root);
12798
12830
  const allowedModels = hc ? new Set(hc.models) : null;
12799
- for (const [name, role] of roles) {
12800
- try {
12801
- if (!resolveRoleSystemPrompt(root, role).trim()) issues.push(`role "${name}": system prompt is empty (define systemPromptFile (recommended) or systemPrompt)`);
12802
- } catch (err) {
12803
- issues.push(`role "${name}": ${String(err?.message ?? err)}`);
12804
- }
12805
- for (const m of role.msms ?? []) if (!registered.has(m)) issues.push(`role "${name}": msms "${m}" is not registered in mech-registry.json`);
12806
- if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
12831
+ for (const p of profiles) {
12832
+ if (!(hasTrajectoryPrompt(root, p.dirName) || (() => {
12833
+ try {
12834
+ return resolveProfileSystemPrompt(root, p).trim() !== "";
12835
+ } catch (err) {
12836
+ issues.push(`trajectory "${p.code}": ${String(err?.message ?? err)}`);
12837
+ return true;
12838
+ }
12839
+ })())) issues.push(`trajectory "${p.code}": no trajectory prompt (add ${TRAJECTORY_PROMPT_FILENAME} in the trajectory dir — recommended — or the legacy systemPromptFile/systemPrompt)`);
12840
+ for (const m of p.msms) if (!registered.has(m)) issues.push(`trajectory "${p.code}": msms "${m}" is not registered in mech-registry.json`);
12841
+ if (p.model && allowedModels && !allowedModels.has(p.model)) issues.push(`trajectory "${p.code}": model "${p.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
12807
12842
  }
12808
12843
  return {
12809
12844
  ok: issues.length === 0,
12810
12845
  issues,
12811
- roleCount: roles.size
12846
+ roleCount: profiles.length
12812
12847
  };
12813
12848
  }
12814
- /** 列出当前 CCC 已定义角色(名 / 模型 / msms / tools / 轨迹纪律摘要) */
12849
+ /** 列出当前 CCC 的 skiff 轨迹(编码 / 模型 / msms / tools / 纪律摘要 / 提示词来源) */
12815
12850
  function listSkiffRoles(root) {
12816
- const roles = readSkiffRoles(root);
12817
- if (roles.size === 0) return {
12851
+ const profiles = listSkiffProfiles(root);
12852
+ if (profiles.length === 0) return {
12818
12853
  roles: [],
12819
- note: "no skiff roles defined (skiff disabled)"
12820
- };
12821
- const items = [...roles.entries()].map(([name, role]) => ({
12822
- name,
12823
- model: role.model ?? "(cheap-worker default)",
12824
- msms: role.msms ?? [],
12825
- tools: role.tools ?? [],
12826
- cognitiveTrajectory: {
12827
- session: role.cognitiveTrajectory?.session === true,
12828
- keeper: role.cognitiveTrajectory?.keeper === true,
12829
- rebuild: role.cognitiveTrajectory?.rebuild === true
12830
- },
12831
- promptSource: systemPromptSource(role),
12832
- ...role.systemPromptFile ? { systemPromptFile: role.systemPromptFile } : {}
12854
+ note: `no skiff trajectories in this CCC (no trajectory has ${SKIFF_PROFILE_FILENAME})`
12855
+ };
12856
+ const items = profiles.map((p) => ({
12857
+ name: p.code,
12858
+ dirName: p.dirName,
12859
+ model: p.model ?? "(cheap-worker default)",
12860
+ msms: p.msms,
12861
+ tools: p.tools,
12862
+ cognitiveTrajectory: profileCognitiveTrajectory(p),
12863
+ promptSource: profileSystemPromptSource(p),
12864
+ ...p.systemPromptFile ? { systemPromptFile: p.systemPromptFile } : {}
12833
12865
  }));
12834
12866
  return {
12835
12867
  roles: items,
@@ -12837,29 +12869,29 @@ function listSkiffRoles(root) {
12837
12869
  };
12838
12870
  }
12839
12871
  /**
12840
- * 应用当前 skiff 配置(v1.25.3,S142 用户:改了 json 应该有个生效机制)。
12872
+ * 应用当前 skiff 配置(S142 用户:改了 json 应该有个生效机制)。
12841
12873
  *
12842
- * 语义:配置是**每次请求实时读取**的(v1.25.2)——apply 不是"推送",而是**显式校验 + 应用确认**:
12843
- * ① 校验配置合法(复用 validate:msms 注册 / model 白名单 / systemPrompt 非空)
12874
+ * 语义:约束面是**每轮实时读取**的 ⇒ apply 不是"推送",而是**显式校验 + 应用确认**:
12875
+ * ① 校验合法(复用 validate:msms 注册 / model 白名单 / 提示词非空)
12844
12876
  * ② 非法 → 报告问题清单,不应用(提示修复后重跑 apply)
12845
- * ③ 合法 → 应用确认:绑定 CCC + 角色清单 + 说明(调试页刷新即生效)
12877
+ * ③ 合法 → 应用确认:绑定 CCC + 轨迹清单 + 说明(下一轮即生效)
12846
12878
  */
12847
12879
  function applySkiffConfig(root) {
12848
12880
  const validation = validateSkiffConfig(root);
12849
- const roles = readSkiffRoles(root);
12881
+ const profiles = listSkiffProfiles(root);
12850
12882
  if (!validation.ok) return {
12851
12883
  applied: false,
12852
12884
  cccRoot: root,
12853
- roleCount: roles.size,
12885
+ roleCount: profiles.length,
12854
12886
  issues: validation.issues,
12855
12887
  hint: "fix the issues above, then run container_admin role apply again"
12856
12888
  };
12857
12889
  return {
12858
12890
  applied: true,
12859
12891
  cccRoot: root,
12860
- roleCount: roles.size,
12861
- roles: [...roles.keys()],
12862
- note: "roles are read live from .opencode/serenity.json — the skiff debug page reflects changes on refresh; skill loading is always available to skiff sessions (not whitelisted)"
12892
+ roleCount: profiles.length,
12893
+ roles: profiles.map((p) => p.code),
12894
+ note: `constraints are read live from each trajectory's ${SKIFF_PROFILE_FILENAME} — they take effect on the next turn (no restart); ${SKIFF_PROFILE_HINT}`
12863
12895
  };
12864
12896
  }
12865
12897
  //#endregion
@@ -13002,18 +13034,18 @@ function createContainerAdminTool(ctx) {
13002
13034
  });
13003
13035
  }
13004
13036
  //#endregion
13005
- //#region src/im-bridge.ts
13037
+ //#region src/im/channels.ts
13006
13038
  /**
13007
- * im-bridge.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
13039
+ * im/channels.ts — IM 发送能力层(v1.31.0,S142 用户洞察)
13008
13040
  *
13009
13041
  * 用户原话:「既然微信桥是我们 ACC 提供的,那么 weixin-send 应该是我们 ACC 提供的能力,
13010
13042
  * 当用户配置了微信桥则可用,不配置则不可见」+「设定一个复杂的多层工具,叫 im-bridge,
13011
13043
  * 里面有微信,然后保留后续可能兼容别的 im 的空间」。
13012
13044
  *
13013
13045
  * 分层(E↑):
13014
- * `tools/im-bridge.ts`(工具面:参数 → 结果渲染)
13046
+ * `tools/im/channels.ts`(工具面:参数 → 结果渲染)
13015
13047
  * → **本模块**(通道注册表 + 可见性判据 + 动作分发 + 错误码翻译)
13016
- * → 通道实现(`im-weixin.ts`:协议/凭据/账号/记录)
13048
+ * → 通道实现(`im/weixin-channel.ts`:协议/凭据/账号/记录)
13017
13049
  *
13018
13050
  * 边界(R↓):
13019
13051
  * - **本模块零宿主依赖**(不 import cordis/agent)——可见性判定与分发都是纯逻辑,可独立单测。
@@ -13286,16 +13318,15 @@ function isWriteTool(toolName, action) {
13286
13318
  */
13287
13319
  function decideGuard(input) {
13288
13320
  const { root, toolName, safeModeOn, blacklist, pathArg, action } = input;
13289
- if (input.skiffSessionId !== void 0 && isSkiffSessionId(input.skiffSessionId)) {
13321
+ if (input.skiffProfile !== void 0 && input.skiffProfile !== null) {
13322
+ const profile = input.skiffProfile;
13290
13323
  if (toolName === "skill") return { kind: "allow" };
13291
- const roleName = input.skiffSessionId ? skiffRoleFor$1(input.skiffSessionId) : null;
13292
- const role = roleName ? readSkiffRoles(root).get(roleName) : void 0;
13293
- if ((role?.msms ?? []).includes(toolName)) return {
13294
- deny: `"${toolName}" is a registered MSM, not a direct tool in this role — call it through the msm tool: msm("${toolName}", ["<args>"]) (pass "--help" or inspect=true for usage)`,
13324
+ if (profile.msms.includes(toolName)) return {
13325
+ deny: `"${toolName}" is a registered MSM, not a direct tool in this trajectory — call it through the msm tool: msm("${toolName}", ["<args>"]) (pass "--help" or inspect=true for usage)`,
13295
13326
  kind: "deny"
13296
13327
  };
13297
- if (!roleToolWhitelist(role).has(toolName)) return {
13298
- deny: "tool not allowed in this skiff role",
13328
+ if (!profileToolWhitelist(profile).has(toolName)) return {
13329
+ deny: "tool not allowed in this skiff trajectory",
13299
13330
  kind: "deny"
13300
13331
  };
13301
13332
  }
@@ -13450,7 +13481,7 @@ function syncSafeModeRestriction(agent, root) {
13450
13481
  });
13451
13482
  writeRestrictDiag(root);
13452
13483
  }
13453
- /** 条件可见的 IM 工具名(与 tools/im-bridge.ts 的注册名一致) */
13484
+ /** 条件可见的 IM 工具名(与 tools/im/channels.ts 的注册名一致) */
13454
13485
  const IM_BRIDGE_TOOL_NAME = "im-bridge";
13455
13486
  /** agent key → restrict disposer(im-bridge 隐藏状态;key 同安全模式 = 会话 id) */
13456
13487
  const imBridgeRestrictions = /* @__PURE__ */ new Map();
@@ -13582,26 +13613,25 @@ const exclusiveRestrictions = /* @__PURE__ */ new Map();
13582
13613
  */
13583
13614
  const skiffRoleRestrictions = /* @__PURE__ */ new Map();
13584
13615
  /**
13585
- * 同步 **Skiff 会话的工具面**:把角色白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
13616
+ * 同步 **skiff 会话的工具面**:把白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
13586
13617
  *
13587
13618
  * ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
13588
13619
  *
13589
- * 在它之前,skiff 角色白名单**只由运行时守卫拦截**(`decideGuard` 的 skiff 分支):
13590
- * 工具全部出现在模型 schema 里,调用时才被 `tool not allowed in this skiff role` 打回。
13591
- * ⇒ 两个真实后果:
13620
+ * 在它之前,skiff 白名单**只由运行时守卫拦截**(`decideGuard` 的 skiff 分支):
13621
+ * 工具全部出现在模型 schema 里,调用时才被打回。⇒ 两个真实后果:
13592
13622
  * ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
13593
13623
  * 看不出"是不在清单里"还是"临时性拒绝");
13594
- * ② **配置变动在 schema 上永远不生效** —— 改 `skiff.roles.<r>.tools` 只影响"调用是否放行",
13595
- * 不影响"模型看得见什么"。这正是 owner 报的"工具清单不热加载"。
13624
+ * ② **配置变动在 schema 上永远不生效** —— 改 `skiff.json` 的 `tools` 只影响"调用是否放行"。
13596
13625
  *
13597
13626
  * ## 判据与降级
13598
13627
  *
13599
- * - deny 集合 = **`REGISTERED_TOOLS` − 角色白名单**(只对**我们自己注册的**工具算,
13628
+ * - deny 集合 = **`REGISTERED_TOOLS` − 白名单**(只对**我们自己注册的**工具算,
13600
13629
  * 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
13601
13630
  * - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
13602
13631
  * 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
13603
- * - 读不到角色(配置删了/角色被移除)⇒ **不动**(fail-open):此时守卫仍会拦,
13604
- * 最坏是"又变成看得见但调不动",而不是把工具全删光。
13632
+ * - 读不到约束面(不是 skiff / 配置删了)⇒ **撤销既有隐藏**(回到"无约束"):
13633
+ * ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是 skiff 的会话上
13634
+ * (这正是 owner 说的"删掉 `skiff.json` 就该立刻恢复")。
13605
13635
  * - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
13606
13636
  *
13607
13637
  * @param agent 目标会话
@@ -13609,20 +13639,20 @@ const skiffRoleRestrictions = /* @__PURE__ */ new Map();
13609
13639
  */
13610
13640
  function syncSkiffRoleToolVisibility(agent, root) {
13611
13641
  const sessionId = agent.session.id ?? "global";
13612
- if (!isSkiffSessionId(sessionId)) return;
13613
- const roleName = skiffRoleFor$1(sessionId);
13614
- if (!roleName) return;
13615
- let allowed;
13616
- try {
13617
- const role = readSkiffRoles(root).get(roleName);
13618
- if (!role) return;
13619
- allowed = roleToolWhitelist(role);
13620
- } catch {
13642
+ const profile = skiffProfileForSession(root, sessionId);
13643
+ const existing = skiffRoleRestrictions.get(sessionId);
13644
+ if (profile === null) {
13645
+ if (existing !== void 0) {
13646
+ try {
13647
+ existing.dispose();
13648
+ } catch {}
13649
+ skiffRoleRestrictions.delete(sessionId);
13650
+ }
13621
13651
  return;
13622
13652
  }
13653
+ const allowed = profileToolWhitelist(profile);
13623
13654
  const resident = REGISTERED_TOOLS.slice(0, REGISTERED_TOOLS.length - 3);
13624
13655
  const tiers = [REGISTERED_TOOLS, resident];
13625
- const existing = skiffRoleRestrictions.get(sessionId);
13626
13656
  for (const names of tiers) {
13627
13657
  const deny = names.filter((n) => !allowed.has(n));
13628
13658
  const signature = [...deny].sort().join(",");
@@ -13857,6 +13887,7 @@ function registerGuards(ctx, opts = {}) {
13857
13887
  const pathArg = extractPathArg(exec);
13858
13888
  const action = extractAction(exec);
13859
13889
  const sessionId = exec?.agent?.session?.id;
13890
+ const skiffProfile = skiffProfileForSession(root, sessionId);
13860
13891
  return decideGuard({
13861
13892
  root,
13862
13893
  toolName: exec.name,
@@ -13864,7 +13895,7 @@ function registerGuards(ctx, opts = {}) {
13864
13895
  blacklist,
13865
13896
  pathArg,
13866
13897
  action,
13867
- skiffSessionId: sessionId
13898
+ skiffProfile
13868
13899
  });
13869
13900
  };
13870
13901
  ctx.on("tools/pre-execute", async (exec, next) => {
@@ -14099,7 +14130,9 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
14099
14130
  promoted: true
14100
14131
  };
14101
14132
  const loopSid = session.id;
14102
- if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || isSkiffSessionId(loopSid))) return {
14133
+ const loopCwd = session.header?.cwd ?? process.cwd();
14134
+ const loopRoot = cccRootForCwd(loopCwd);
14135
+ if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || loopRoot !== null && isSkiffSession(loopRoot, loopSid))) return {
14103
14136
  boundary: -1,
14104
14137
  promoted: true
14105
14138
  };
@@ -14212,12 +14245,13 @@ function registerBootstrap(ctx) {
14212
14245
  });
14213
14246
  ctx.on("agent/inbox/inserted", ({ agent, message }) => {
14214
14247
  try {
14215
- if (!agentRoot(agent)) return;
14248
+ const root = agentRoot(agent);
14249
+ if (!root) return;
14216
14250
  const session = agent.session;
14217
14251
  const depth = session?.header?.delegationDepth ?? 0;
14218
14252
  const sid = typeof session?.id === "string" ? session.id : void 0;
14219
14253
  if (sid === void 0) return;
14220
- if (sid.startsWith("cheap-worker-") || isSkiffSessionId(sid)) return;
14254
+ if (sid.startsWith("cheap-worker-") || isSkiffSession(root, sid)) return;
14221
14255
  if (depth === 0) {
14222
14256
  if (hasUserMessageHistory(session)) return;
14223
14257
  } else if (anchoredSessions.has(sid)) return;
@@ -14347,9 +14381,9 @@ function registerBootstrap(ctx) {
14347
14381
  }, { prepend: true });
14348
14382
  }
14349
14383
  //#endregion
14350
- //#region src/cognitive-trajectory-assistant.ts
14384
+ //#region src/trajectory/assistant.ts
14351
14385
  /**
14352
- * cognitive-trajectory-assistant.ts — 关卡化注入的统一命名与风格门面(v0.3,S142)
14386
+ * trajectory/assistant.ts — 关卡化注入的统一命名与风格门面(v0.3,S142)
14353
14387
  *
14354
14388
  * 概念(用户拍板 2026-09-05):dsp 全部"过程中动态提示注入"统一命名为
14355
14389
  * cognitive-trajectory-assistant(轨迹助航员)。关卡设计思想塑造**结构与时机**
@@ -14409,7 +14443,7 @@ const ACK_PREFIX = `${ASSISTANT_PREFIX}-recorded`;
14409
14443
  * ⇒ 分家:**只用一次的交界材料**写进这份**临时便签**(无格式义务、无留档义务、可随时丢弃)。
14410
14444
  *
14411
14445
  * ## 🔴 三处引用同一常量(单一真相源)
14412
- * **写侧** = `seams/keeper.ts` 的 `rebuildReminderText()`|**读侧** = `rebuild.ts` 的重建锚点|
14446
+ * **写侧** = `seams/keeper.ts` 的 `rebuildReminderText()`|**读侧** = `trajectory/rebuild.ts` 的重建锚点|
14413
14447
  * **体积提醒** = `seams/keeper.ts` 的 `rebuildTodoReminderText()`。
14414
14448
  * 文件名一旦漂移,写侧写的东西读侧就找不到 —— **且没有任何机械信号会报警**。
14415
14449
  *
@@ -14488,7 +14522,7 @@ function reminderText(code) {
14488
14522
  * 使用者若要查看当前占用/阈值:看面板(`rebuildThresholdK` 滑块 + 状态显示)——那是给人看的。
14489
14523
  *
14490
14524
  * v1.31.1 交接协议(S142 用户需求"要求 LLM 将当前手头事项写在 SESSION.md 尾部,并要求
14491
- * rebuild 后去读并处理"):**写侧** = 这里;**读侧** = rebuild 锚点(rebuild.ts)。
14525
+ * rebuild 后去读并处理"):**写侧** = 这里;**读侧** = rebuild 锚点(trajectory/rebuild.ts)。
14492
14526
  * 两侧共用 cognitive-trajectory-assistant 的常量(单一真相源)。
14493
14527
  *
14494
14528
  * 🔴 **v1.49.0(owner 2026-09-26 裁决:"砍掉 in-flight,这正是目的")**:写侧**不再**指向
@@ -14820,22 +14854,23 @@ function agentScope(agent) {
14820
14854
  * - cheap-worker 杂工:sessionId 固定 `cheap-worker-` 前缀(tools/cheap-worker.ts 生成)
14821
14855
  * 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
14822
14856
  */
14823
- function shouldAutoRestore(agent) {
14857
+ function shouldAutoRestore(agent, root) {
14824
14858
  const session = agent.session;
14825
14859
  if (!session) return false;
14826
14860
  if (session.header?.origin === "subagent") return false;
14827
14861
  if (session.header?.parentSession) return false;
14828
14862
  if (session.id?.startsWith("cheap-worker-")) return false;
14829
- if (isSkiffSessionId(session.id)) return false;
14863
+ const cccRoot = root !== void 0 ? root : cccRootForCwd(session.header?.cwd ?? process.cwd());
14864
+ if (cccRoot && isSkiffSession(cccRoot, session.id)) return false;
14830
14865
  return true;
14831
14866
  }
14832
14867
  /**
14833
14868
  * 恢复触发判定(S134 泄漏修复 v1.16.13):根会话 **且已有对话历史**(续跑/恢复的会话)
14834
- * 才自动恢复上次激活的宁静号会话——全新会话(新任务,如 apaas-26116)无历史 → 不恢复,
14869
+ * 才自动恢复上次激活的宁静号会话——全新会话(新任务)无历史 → 不恢复,
14835
14870
  * 避免把过去的 SESSION 上下文注入新任务(跨任务污染)。
14836
14871
  */
14837
- function shouldRestoreActive(agent) {
14838
- if (!shouldAutoRestore(agent)) return false;
14872
+ function shouldRestoreActive(agent, root) {
14873
+ if (!shouldAutoRestore(agent, root)) return false;
14839
14874
  return sessionEvents$1(agent.session).length > 0;
14840
14875
  }
14841
14876
  function registerContext(ctx, opts = {}) {
@@ -14849,7 +14884,7 @@ function registerContext(ctx, opts = {}) {
14849
14884
  } catch {}
14850
14885
  if (!root) return;
14851
14886
  const key = agentKey(agent);
14852
- if (isSkiffSessionId(key)) return;
14887
+ if (isSkiffSession(root, key)) return;
14853
14888
  registerEntrySkillSection(agent, root);
14854
14889
  registerCognitiveTrajectorySkillSection(agent, root);
14855
14890
  const scope = agentScope(agent);
@@ -14935,7 +14970,7 @@ function registerContext(ctx, opts = {}) {
14935
14970
  if (root) try {
14936
14971
  syncSkiffRoleToolVisibility(agent, root);
14937
14972
  } catch {}
14938
- if (isSkiffSessionId(key)) return downstream;
14973
+ if (root && isSkiffSession(root, key)) return downstream;
14939
14974
  try {
14940
14975
  syncCccToolVisibility(agent, root);
14941
14976
  } catch {}
@@ -14979,12 +15014,12 @@ function registerCompactRetention(ctx, opts = {}) {
14979
15014
  ctx.on("session/event", (session, event) => {
14980
15015
  if (event.type !== "compaction/end") return;
14981
15016
  if (event.data.error) return;
14982
- if (isSkiffSessionId(String(session.id ?? ""))) return;
14983
15017
  const agent = ctx.agents.get(session.id);
14984
15018
  if (!agent) return;
14985
15019
  const cwd = agent.session?.header?.cwd ?? process.cwd();
14986
15020
  const root = cccRootForCwd(cwd);
14987
15021
  if (!root) return;
15022
+ if (isSkiffSession(root, String(session.id ?? ""))) return;
14988
15023
  try {
14989
15024
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
14990
15025
  } catch {}
@@ -15051,9 +15086,9 @@ function loadOpencodeSkill(skillDir) {
15051
15086
  };
15052
15087
  }
15053
15088
  //#endregion
15054
- //#region src/status.ts
15089
+ //#region src/surface/status.ts
15055
15090
  /**
15056
- * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
15091
+ * surface/status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
15057
15092
  *
15058
15093
  * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / cheap-worker 模型。
15059
15094
  * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
@@ -15188,7 +15223,7 @@ function setSafeMode(root, on) {
15188
15223
  return { on: isSafeModeOn(root) };
15189
15224
  }
15190
15225
  //#endregion
15191
- //#region src/api.ts
15226
+ //#region src/surface/http.ts
15192
15227
  const ROUTE_PATH = "/serenity/status";
15193
15228
  const UPLOAD_PATH = "/serenity/image-upload";
15194
15229
  const FILE_UPLOAD_PATH = "/serenity/file-upload";
@@ -15329,7 +15364,7 @@ function requireWebUi(req, res, message) {
15329
15364
  * 它就会**静默返回另一个 CCC 的路径**,于是面板把**别人的轨迹/技能/安全模式**当成你的显示出来。
15330
15365
  * 这比"显示未激活"危险得多 —— 用户会据此做判断(以为自己的 safe-mode 开着、以为那是自己的轨迹)。
15331
15366
  *
15332
- * 修法与既有先例一致(`rebuild.ts` 对"跨轨迹猜测"的裁决:**响亮失败优于静默接错**):
15367
+ * 修法与既有先例一致(`trajectory/rebuild.ts` 对"跨轨迹猜测"的裁决:**响亮失败优于静默接错**):
15333
15368
  * **多个候选 ⇒ 不猜**。只有当 live sessions 里的 CCC 根**唯一**时才采用它
15334
15369
  * (那正是 v1.22.2 要救的场景:单 CCC 部署 + 远程访问缺 sessionId);
15335
15370
  * 一旦出现**两个及以上**不同 CCC 根,就退回进程 cwd(→ 面板显示"未激活"),绝不挑一个顶替。
@@ -15539,11 +15574,11 @@ function registerStatusApi(ctx, opts = {}) {
15539
15574
  sendJson$3(res, 400, { error: chk.error });
15540
15575
  return;
15541
15576
  }
15542
- const { readWeixinSettings } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15543
- const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
15577
+ const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15578
+ const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
15544
15579
  const settings = readWeixinSettings(chk.root);
15545
15580
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
15546
- const { readWeixinCredential } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15581
+ const { readWeixinCredential } = await import("./route-f7JlinIt.js").then((n) => n.s);
15547
15582
  const accounts = (settings.accounts ?? []).map((a) => ({
15548
15583
  accountId: a.accountId,
15549
15584
  name: a.name ?? void 0,
@@ -15572,10 +15607,10 @@ function registerStatusApi(ctx, opts = {}) {
15572
15607
  }
15573
15608
  const root = chk.root;
15574
15609
  if (body.action === "login-start") {
15575
- const { fetchQRCode } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15610
+ const { fetchQRCode } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
15576
15611
  const { randomUUID } = await import("node:crypto");
15577
15612
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
15578
- const { readWeixinSettings } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15613
+ const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15579
15614
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
15580
15615
  const loginKey = randomUUID();
15581
15616
  weixinLogins.set(loginKey, {
@@ -15596,24 +15631,36 @@ function registerStatusApi(ctx, opts = {}) {
15596
15631
  sendJson$3(res, 400, { error: "missing accountId" });
15597
15632
  return;
15598
15633
  }
15599
- const { removeWeixinAccount } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15600
- const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15634
+ const { removeWeixinAccount } = await import("./route-f7JlinIt.js").then((n) => n.s);
15635
+ const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15601
15636
  removeWeixinAccount(root, body.accountId);
15602
15637
  syncCccBridge(ctx, root);
15603
15638
  sendJson$3(res, 200, { removed: body.accountId });
15604
15639
  return;
15605
15640
  }
15606
15641
  if (body.action === "save-routes") {
15607
- const routes = body.routes ?? [];
15608
- if (!Array.isArray(routes) || routes.some((r) => typeof r?.user !== "string" || typeof r?.role !== "string" || r.user === "" || r.role === "")) {
15609
- sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
15642
+ const raw = body.routes ?? [];
15643
+ if (!Array.isArray(raw)) {
15644
+ sendJson$3(res, 400, { error: "invalid routes (expected [{user, trajectory}, ...])" });
15610
15645
  return;
15611
15646
  }
15612
- const { saveWeixinRoutes } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15613
- const { readSkiffRoles } = await import("./skiff-role-cwxAt0gE.js").then((n) => n.d);
15614
- const roles = readSkiffRoles(root);
15615
- for (const r of routes) if (!roles.has(r.role)) {
15616
- sendJson$3(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
15647
+ const routes = [];
15648
+ for (const r of raw) {
15649
+ const user = typeof r?.user === "string" ? r.user : "";
15650
+ const code = typeof r?.trajectory === "string" && r.trajectory !== "" ? r.trajectory : typeof r?.role === "string" ? r.role : "";
15651
+ if (user === "" || code === "") {
15652
+ sendJson$3(res, 400, { error: "invalid routes (each row needs non-empty user and trajectory)" });
15653
+ return;
15654
+ }
15655
+ routes.push({
15656
+ user,
15657
+ trajectory: code
15658
+ });
15659
+ }
15660
+ const { saveWeixinRoutes } = await import("./route-f7JlinIt.js").then((n) => n.s);
15661
+ const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
15662
+ for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
15663
+ sendJson$3(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
15617
15664
  return;
15618
15665
  }
15619
15666
  saveWeixinRoutes(root, routes);
@@ -15622,8 +15669,8 @@ function registerStatusApi(ctx, opts = {}) {
15622
15669
  }
15623
15670
  if (body.action === "set-enabled") {
15624
15671
  const enabled = body.enabled === true;
15625
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15626
- const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15672
+ const { readWeixinSettings, setWeixinEnabled } = await import("./route-f7JlinIt.js").then((n) => n.s);
15673
+ const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15627
15674
  const settings = readWeixinSettings(root);
15628
15675
  if (enabled && (settings.accounts ?? []).length === 0) {
15629
15676
  sendJson$3(res, 400, { error: "启用前请先扫码绑定至少一个账号" });
@@ -15632,7 +15679,7 @@ function registerStatusApi(ctx, opts = {}) {
15632
15679
  setWeixinEnabled(root, enabled);
15633
15680
  if (enabled) syncCccBridge(ctx, root);
15634
15681
  else {
15635
- const { stopCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15682
+ const { stopCccBridge } = await Promise.resolve().then(() => bridge_exports);
15636
15683
  stopCccBridge(root);
15637
15684
  }
15638
15685
  sendJson$3(res, 200, { enabled });
@@ -15665,8 +15712,8 @@ function registerStatusApi(ctx, opts = {}) {
15665
15712
  sendJson$3(res, 200, { status: "expired" });
15666
15713
  return;
15667
15714
  }
15668
- const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15669
- const { readWeixinSettings } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15715
+ const { pollQRStatus } = await import("./ilink-DEpmE3yi.js").then((n) => n.a);
15716
+ const { readWeixinSettings } = await import("./route-f7JlinIt.js").then((n) => n.s);
15670
15717
  const settings = readWeixinSettings(login.root);
15671
15718
  const status = await pollQRStatus({
15672
15719
  baseUrl: void 0,
@@ -15681,8 +15728,8 @@ function registerStatusApi(ctx, opts = {}) {
15681
15728
  });
15682
15729
  return;
15683
15730
  }
15684
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-D_DvgPqy.js").then((n) => n.u);
15685
- const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15731
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-f7JlinIt.js").then((n) => n.s);
15732
+ const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
15686
15733
  const accountId = nextWeixinAccountId(settings);
15687
15734
  upsertWeixinAccount(login.root, {
15688
15735
  accountId,
@@ -15730,7 +15777,7 @@ function registerStatusApi(ctx, opts = {}) {
15730
15777
  const id = s.id ?? s.header?.id;
15731
15778
  if (id) liveIds.add(id);
15732
15779
  }
15733
- const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await Promise.resolve().then(() => session_cleanup_exports);
15780
+ const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await Promise.resolve().then(() => cleanup_exports);
15734
15781
  const root = sessionsRootDir();
15735
15782
  if (req.method === "GET") {
15736
15783
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
@@ -15786,9 +15833,9 @@ function requireCcc(root) {
15786
15833
  };
15787
15834
  }
15788
15835
  //#endregion
15789
- //#region src/voyage-vocab.ts
15836
+ //#region src/surface/voyage/vocab.ts
15790
15837
  /**
15791
- * voyage-vocab.ts — 航行页词表改为**按当前 CCC 运行时注入**(纯逻辑,零 DSH 依赖)
15838
+ * surface/voyage/vocab.ts — 航行页词表改为**按当前 CCC 运行时注入**(纯逻辑,零 DSH 依赖)
15792
15839
  *
15793
15840
  * ## 为什么需要它(owner 2026-10-18 报的缺陷)
15794
15841
  * 资产 `assets/serenity-voyage.html` 的词表是**构建期扫描容器写死的常量**:
@@ -15906,7 +15953,7 @@ function injectVoyageVocab(html, live) {
15906
15953
  return html.replace(VOCAB_LINE_RE, () => `var VOCAB = ${JSON.stringify(merged)};`);
15907
15954
  }
15908
15955
  //#endregion
15909
- //#region src/voyage-page.ts
15956
+ //#region src/surface/voyage/page.ts
15910
15957
  const VOYAGE_PATH = "/serenity/voyage";
15911
15958
  /**
15912
15959
  * 资产相对包根的路径。
@@ -15915,9 +15962,15 @@ const VOYAGE_PATH = "/serenity/voyage";
15915
15962
  * `dsh-develop pack-check` 的 `required` 已把它列为发布前硬断言。
15916
15963
  */
15917
15964
  const VOYAGE_ASSET = "assets/serenity-voyage.html";
15918
- /** 包根 = 本模块所在目录的上一级(构建产物在 `lib/`,与 `assets/` 同级;同 constants.ts 定位法) */
15965
+ /**
15966
+ * 包根 = 含本包 `package.json` 的目录。
15967
+ * 🔴 **不许写死层数**:本模块自身位置随目录重构变化(曾 `src/surface/voyage/page.ts`,现
15968
+ * `src/surface/voyage/page.ts`),而发布态是从打包入口 `lib/index.js` 反推 ⇒ 两种布局深度不同,
15969
+ * 写死 `'..'` 必在其中一个布局下静默指错(实测:搬进 `surface/voyage/` 后路由直接读不到资产 → 500)。
15970
+ * ⇒ 交给 {@link findPackageRoot} 按**内容**定位。
15971
+ */
15919
15972
  function packageRootPath() {
15920
- return join(dirname(fileURLToPath(import.meta.url)), "..");
15973
+ return findPackageRoot() ?? join(dirname(fileURLToPath(import.meta.url)), "..");
15921
15974
  }
15922
15975
  /** 资产绝对路径(导出以便测试与 pack-check 共用同一判据来源) */
15923
15976
  function voyageAssetPath() {
@@ -15933,7 +15986,7 @@ function loadVoyageHtml(assetPath = voyageAssetPath()) {
15933
15986
  }
15934
15987
  return readFileSync(assetPath, "utf-8");
15935
15988
  }
15936
- /** 发 JSON(错误分支用;与 api.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
15989
+ /** 发 JSON(错误分支用;与 surface/http.ts 的 sendJson 同形,但本模块不依赖它,保持零耦合) */
15937
15990
  function sendJson$2(res, status, body) {
15938
15991
  res.statusCode = status;
15939
15992
  res.setHeader("content-type", "application/json; charset=utf-8");
@@ -15942,7 +15995,7 @@ function sendJson$2(res, status, body) {
15942
15995
  /**
15943
15996
  * 注册 `/serenity/voyage`(GET)。
15944
15997
  * @param opts.assetPath 覆盖资产路径(测试用;生产不传)
15945
- * @param opts.resolveRoot 解析本次请求所属 CCC 根(生产由 api.ts 的 resolveWorkspace 提供)。
15998
+ * @param opts.resolveRoot 解析本次请求所属 CCC 根(生产由 surface/http.ts 的 resolveWorkspace 提供)。
15946
15999
  * 🔴 **2026-10-18 新增**:词表按请求所属 CCC **运行时注入**(owner 报「在别的 CCC 点开
15947
16000
  * 显示的还是 home-serenity 的信息」)。不传 ⇒ 不注入(保持资产原样 = 旧行为)。
15948
16001
  */
@@ -16534,9 +16587,9 @@ function summarizeHostContract(report) {
16534
16587
  return `${required.length > 0 ? `host contract BROKEN (${required.length} required)` : `host contract degraded (${optional.length} optional)`}: ${report.issues.map((i) => i.detail).join("; ")}`;
16535
16588
  }
16536
16589
  //#endregion
16537
- //#region src/face-host.ts
16590
+ //#region src/surface/faces.ts
16538
16591
  /**
16539
- * face-host.ts — **面宿主**:插件自起 HTTP listener 的唯一生命周期处(C4 块 A,S142 2026-09-15)
16592
+ * surface/faces.ts — **面宿主**:插件自起 HTTP listener 的唯一生命周期处(C4 块 A,S142 2026-09-15)
16540
16593
  *
16541
16594
  * ## 现状 → 本模块要收掉什么(取证:`…/acc-c4c5-current-state.md` §①/§③ 3.1)
16542
16595
  *
@@ -16545,7 +16598,7 @@ function summarizeHostContract(report) {
16545
16598
  * ⇒ 仍保有面 B/C/D 三个;本段保留"当年是四个"的记述,因为下方"两套拆卸"的病史由它解释。
16546
16599
  * 此前四处各写一份同款样板:`createServer` + `server.once('error')` +
16547
16600
  * `listen(port, host)` + 模块级 `active` 变量 + `close()`;拆卸也分裂成**两套**——
16548
- * `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway.ts` 与 `weixin-send-api.ts`
16601
+ * `seams/lifecycle.ts` 有一个聚合 disposer,而 `gateway/server.ts` 与 `im/send.ts`
16549
16602
  * **各自另注册**一个(D/E 干脆没有自己的 disposer)。
16550
16603
  *
16551
16604
  * 本模块把"listener 生命周期"收成一处:**active 表 + 统一 listen/close + 统一错误与端口占用
@@ -16554,7 +16607,7 @@ function summarizeHostContract(report) {
16554
16607
  * ## 🔴 共享的是「生命周期与样板」,**绝不是**「路由 / 监听器 / 鉴权」
16555
16608
  *
16556
16609
  * 每个面**保留自己的 listener、自己的路由、自己的鉴权**(`handler` 内部原样不动):
16557
- * · **A ↔ C 解耦**(`weixin-send-api.ts:4-7`):3080 会被 3081 原样反代(连请求头),
16610
+ * · **A ↔ C 解耦**(`im/send.ts:4-7`):3080 会被 3081 原样反代(连请求头),
16558
16611
  * 把 3082 的端点挂到 3080 上 = 已登录的外部家庭账号即可冒充 bot 发消息
16559
16612
  * ⇒ 本模块**不提供**"把面挂到别的面"的能力,也不共享路由表;
16560
16613
  * · (原 **E ↔ D** 共用会话核心但各自独立 listener 一条,**随 E 退役删除**);
@@ -16977,16 +17030,16 @@ ${csrf ? `<input type="hidden" name="csrf" value="${csrf}">` : ""}
16977
17030
  </div></body></html>`;
16978
17031
  }
16979
17032
  //#endregion
16980
- //#region src/gateway-proxy.ts
17033
+ //#region src/gateway/proxy.ts
16981
17034
  /**
16982
- * gateway-proxy.ts — F1 双端口网关:代理辅助纯逻辑(v1.22.1-1.22.3 稳定性修复)
17035
+ * gateway/proxy.ts — F1 双端口网关:代理辅助纯逻辑(v1.22.1-1.22.3 稳定性修复)
16983
17036
  *
16984
- * 从 gateway.ts 拆分(S142 熵点治理,v1.22.8):
17037
+ * 从 gateway/server.ts 拆分(S142 熵点治理,v1.22.8):
16985
17038
  * - crypto.randomUUID polyfill(非安全上下文修复,v1.22.0)
16986
17039
  * - 反代请求头构造(信任栅栏修复:Host + Origin 同步改写 loopback,v1.22.1)
16987
17040
  * - workspace 白名单过滤(响应 items 前缀过滤 + create 校验 + 403 RPC 响应,v1.22)
16988
17041
  * - HTML polyfill 注入(幂等 marker)
16989
- * 纯函数设计(可单测);gateway.ts 装配层 import 使用。
17042
+ * 纯函数设计(可单测);gateway/server.ts 装配层 import 使用。
16990
17043
  */
16991
17044
  /**
16992
17045
  * 经第二端口 http://LAN-IP:3081 访问 = 非安全上下文 → DSH client 的
@@ -17120,7 +17173,7 @@ function buildProxyHeaders(reqHeaders, mainPort, bodyOverride) {
17120
17173
  return headers;
17121
17174
  }
17122
17175
  //#endregion
17123
- //#region src/gateway-dsh-auth.ts
17176
+ //#region src/gateway/dsh-auth.ts
17124
17177
  /** 从一组 set-cookie 头提取 `dsh-auth-*` cookie(browser-auth sessionCookie 首段) */
17125
17178
  function pickDshCookie(setCookieHeader) {
17126
17179
  const entries = Array.isArray(setCookieHeader) ? setCookieHeader : setCookieHeader === void 0 ? [] : [setCookieHeader];
@@ -17183,7 +17236,7 @@ function createDshCookieProvider(connection, authority) {
17183
17236
  };
17184
17237
  }
17185
17238
  //#endregion
17186
- //#region src/gateway.ts
17239
+ //#region src/gateway/server.ts
17187
17240
  /** 面名(C4 块 A:listener 生命周期归 `face-host`) */
17188
17241
  const FACE_GATEWAY = "gateway";
17189
17242
  /** 读取请求体(≤ maxBytes;超限 reject) */
@@ -17626,7 +17679,7 @@ function registerGateway(ctx) {
17626
17679
  });
17627
17680
  }
17628
17681
  //#endregion
17629
- //#region src/rebuild.ts
17682
+ //#region src/trajectory/rebuild.ts
17630
17683
  var rebuild_exports = /* @__PURE__ */ __exportAll({
17631
17684
  buildRebuildAnchor: () => buildRebuildAnchor,
17632
17685
  performRebuild: () => performRebuild,
@@ -17704,11 +17757,10 @@ function parseAnchorMdPath(session) {
17704
17757
  return null;
17705
17758
  }
17706
17759
  }
17707
- /** 从 SESSION.md 绝对路径派生会话名(S###;issue 目录回退完整目录名) */
17760
+ /** 从 SESSION.md 绝对路径派生会话名(编码;issue 目录回退完整目录名) */
17708
17761
  function sessionNameFromMdPath(mdPath) {
17709
17762
  const dirName = basename(dirname(mdPath));
17710
- const idMatch = dirName.match(/--S(\d{3,})--/);
17711
- return idMatch ? `S${idMatch[1]}` : dirName;
17763
+ return parseTrajectoryCode(dirName) ?? dirName;
17712
17764
  }
17713
17765
  /**
17714
17766
  * 稳固的 SESSION.md 定位(多层候选 + 存在性校验,**绝不输出虚假路径**):
@@ -17901,9 +17953,8 @@ function renameAfterRebuild(ctx, agent, pending) {
17901
17953
  try {
17902
17954
  const mdPath = pending.mdPath;
17903
17955
  const dirName = basename(dirname(mdPath));
17904
- const idMatch = dirName.match(/--S(\d{3,})--/);
17905
17956
  const info = {
17906
- sessionId: idMatch ? `S${idMatch[1]}` : dirName.replace(/^\d{4}-\d{2}-\d{2}--/, ""),
17957
+ sessionId: parseTrajectoryCode(dirName) ?? dirName.replace(/^\d{4}-\d{2}-\d{2}--/, ""),
17907
17958
  dirName,
17908
17959
  mdPath
17909
17960
  };
@@ -19636,7 +19687,7 @@ f.parseInline;
19636
19687
  b.parse;
19637
19688
  x.lex;
19638
19689
  //#endregion
19639
- //#region src/skiff-debug.ts
19690
+ //#region src/role/skiff/debug.ts
19640
19691
  /** 面名(C4 块 A) */
19641
19692
  const FACE_SKIFF_DEBUG = "skiff-debug";
19642
19693
  /** 监听地址:**只绑 loopback**(本面无认证——仅靠 loopback 挡住外部) */
@@ -20031,12 +20082,12 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
20031
20082
  return;
20032
20083
  }
20033
20084
  const ccc = typeof body.ccc === "string" && body.ccc !== "" ? body.ccc : defaultRoot;
20034
- const roleName = typeof body.role === "string" ? body.role : "";
20085
+ const code = typeof body.role === "string" ? body.role : "";
20035
20086
  const question = typeof body.question === "string" ? body.question : "";
20036
20087
  const sessionId = typeof body.sessionId === "string" && body.sessionId !== "" ? body.sessionId : void 0;
20037
- const role = readSkiffRoles(ccc).get(roleName);
20038
- if (!roleName || !role) {
20039
- sendJson$1(res, 400, { error: `unknown role: ${roleName} (ccc: ${ccc})` });
20088
+ const profile = code !== "" ? readSkiffProfileByCode(ccc, code) : null;
20089
+ if (!profile) {
20090
+ sendJson$1(res, 400, { error: `unknown skiff trajectory: ${code} (no trajectory with code "${code}" has ${SKIFF_PROFILE_FILENAME} in ${ccc})` });
20040
20091
  return;
20041
20092
  }
20042
20093
  if (!question.trim()) {
@@ -20052,22 +20103,21 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
20052
20103
  sendJson$1(res, 400, { error: "session is not recoverable (process restarted or session unknown) — start a new conversation" });
20053
20104
  return;
20054
20105
  }
20055
- if (info.role !== roleName || info.ccc !== ccc) {
20056
- sendJson$1(res, 400, { error: `session "${sessionId}" belongs to role "${info.role}" in another CCC — start a new conversation` });
20106
+ if (info.trajectoryDirName !== profile.dirName || info.ccc !== ccc) {
20107
+ sendJson$1(res, 400, { error: `session "${sessionId}" belongs to trajectory "${info.trajectoryDirName}" in another CCC — start a new conversation` });
20057
20108
  return;
20058
20109
  }
20059
20110
  agent = live;
20060
20111
  continued = true;
20061
20112
  }
20062
20113
  const hc = readCheapWorkerConfig(ccc);
20063
- if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
20064
- const result = await askSkiff(ctx, agent, question, 0);
20114
+ if (!agent) agent = (await createSkiffAgent(ctx, ccc, profile, hc?.defaultModel)).agent;
20115
+ const result = await askSkiff(ctx, agent, question);
20065
20116
  sendJson$1(res, 200, {
20066
20117
  answer: result.answer,
20067
20118
  answer_html: renderSkiffMarkdown(result.answer),
20068
20119
  sessionId: result.sessionId,
20069
- continued,
20070
- cognitiveTrajectory: result.cognitiveTrajectory
20120
+ continued
20071
20121
  });
20072
20122
  return;
20073
20123
  }
@@ -20077,9 +20127,9 @@ async function handle(ctx, defaultRoot, webPort, req, res) {
20077
20127
  }
20078
20128
  }
20079
20129
  //#endregion
20080
- //#region src/output-guard.ts
20130
+ //#region src/guard/filter.ts
20081
20131
  /**
20082
- * output-guard.ts — 最终输出敏感词检测 + 打回重生成(v1.26.3,S142 用户需求)
20132
+ * guard/filter.ts — 最终输出敏感词检测 + 打回重生成(v1.26.3,S142 用户需求)
20083
20133
  *
20084
20134
  * 认知容器对外提供认知结果时,**支撑宁静号自身的凭据与机制**不应透露给用户。
20085
20135
  * 用户拍板方案(讨论稿定稿):不做工具结果 redaction(那是模型工作内存),
@@ -20216,14 +20266,22 @@ function buildRebuke(hits) {
20216
20266
  /** 打回状态(按 agent 会话跟踪连续命中轮数) */
20217
20267
  const rebukeStates = /* @__PURE__ */ new Map();
20218
20268
  //#endregion
20219
- //#region src/output-guard-seam.ts
20220
- /** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
20221
- * skiff-(F4 问答)/ rebuild-(v1.22.4 重建会话)前缀。
20222
- * 🔴 `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
20223
- * 理由与"为何不擅自删"见文件头注释。 */
20224
- function isExternalFaceSession(sessionId) {
20269
+ //#region src/guard/seam.ts
20270
+ /**
20271
+ * 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免)。
20272
+ *
20273
+ * 🔴 **2026-10-19 判据更换**:skiff 那一条从「sessionId 前缀 `skiff-`」改为
20274
+ * **「该会话绑定的轨迹有 `skiff.json`」**(owner 重做令)。前缀判据在新模型下是
20275
+ * **错的**——skiff 的身份是轨迹,不再由 id 前缀表达;继续用前缀会漏掉所有
20276
+ * "挂在 skiff 轨迹上、但 id 不带前缀"的会话(典型 = 微信桥按轨迹派生的会话 id)。
20277
+ *
20278
+ * ⚠️ `rebuild-` 前缀保留(那是另一件事:重建会话同属对外面,与 skiff 无关);
20279
+ * `acp-` 分支保留但**已是死判据**(无生产者;面 E 已于 2026-10-15 退役)——
20280
+ * 理由与"为何不擅自删"见文件头注释。
20281
+ */
20282
+ function isExternalFaceSession(sessionId, root) {
20225
20283
  if (!sessionId) return false;
20226
- return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
20284
+ return isSkiffSession(root, sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
20227
20285
  }
20228
20286
  /** 读会话最后一个 assistant/message 文本(skiff-core 同款)
20229
20287
  * **v1.63.3 起唯一的导出理由消失**:原导出是给 `unattended-seam.ts` 复用的(D77),
@@ -20253,10 +20311,11 @@ function registerOutputGuardHook(ctx) {
20253
20311
  ctx.on("agent/turn-stopping", (payload) => {
20254
20312
  const agent = payload?.agent;
20255
20313
  if (!agent) return;
20256
- if (!isExternalFaceSession(String(agent.session.id ?? ""))) return;
20314
+ const sessionId = String(agent.session.id ?? "");
20257
20315
  const cwd = agent.session.header?.cwd ?? process.cwd();
20258
20316
  const root = cccRootForCwd(cwd);
20259
20317
  if (!root) return;
20318
+ if (!isExternalFaceSession(sessionId, root)) return;
20260
20319
  const id = agent.id;
20261
20320
  const text = lastAssistantText(agent);
20262
20321
  if (!text) return;
@@ -20288,12 +20347,12 @@ function registerOutputGuardHook(ctx) {
20288
20347
  });
20289
20348
  }
20290
20349
  //#endregion
20291
- //#region src/human-channel.ts
20350
+ //#region src/im/inbound.ts
20292
20351
  /**
20293
- * human-channel.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
20352
+ * im/inbound.ts — 人机信道的**机制层**(机器级;2026-09-26 owner 定案 ⇒ 方案 §2.6)
20294
20353
  *
20295
20354
  * ## 为什么单独一个模块(方案 §5-12)
20296
- * 今天"机制"与"渠道实现"混在 `weixin-bridge.ts` 里。P0-2 要立的这条不变量是**机器级**的 ——
20355
+ * 今天"机制"与"渠道实现"混在 `im/bridge.ts` 里。P0-2 要立的这条不变量是**机器级**的 ——
20297
20356
  * 它**跨 CCC**、且与"微信"无关(飞书桥/邮件桥同样要守)⇒ 抽到这里;`weixin-bridge` 只做接线。
20298
20357
  *
20299
20358
  * ## 本模块唯一的不变量
@@ -20314,7 +20373,7 @@ function registerOutputGuardHook(ctx) {
20314
20373
  * ② **订阅表查询 + 入站路由决策**(P0-3:`(账号, 用户) → (CCC, 角色)`)—— 见 §订阅表
20315
20374
  * ③ **机器级留痕**(P0-3:未投递 / 目标不存在 / 表残缺)—— 见 §留痕。
20316
20375
  * 🔴 为什么这些必须机器级:它们**没有目标 CCC** ⇒ 落进任何 CCC 都是"编一个"目标。
20317
- * **不碰**:轮询循环、凭据解析、会话投递 —— 那些是**渠道实现**,留在 `weixin-bridge.ts`。
20376
+ * **不碰**:轮询循环、凭据解析、会话投递 —— 那些是**渠道实现**,留在 `im/bridge.ts`。
20318
20377
  * ⇒ 本模块**零 DSH 依赖**(只有 `node:crypto` / `node:fs`),可独立单测。
20319
20378
  */
20320
20379
  /**
@@ -20664,9 +20723,9 @@ function registerWeixinOutputGuard(ctx) {
20664
20723
  guardActive = true;
20665
20724
  }
20666
20725
  //#endregion
20667
- //#region src/weixin-hook.ts
20726
+ //#region src/im/hook.ts
20668
20727
  /**
20669
- * weixin-hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
20728
+ * im/hook.ts — 微信桥消息记录 hook(F4c-3 扩展,v1.27.13 用户需求)
20670
20729
  *
20671
20730
  * 需求:微信桥发生的所有消息记录,支持 hook——允许 CCC 通过自行编写 hook
20672
20731
  * 脚本进行**持久性保存**(存哪/存成什么/是否入库,全归 CCC 决定——ACC 不绑定存储)。
@@ -20823,8 +20882,107 @@ function invokeWeixinHook(root, hookRel, event) {
20823
20882
  return activeRunner(root, hookRel, event);
20824
20883
  }
20825
20884
  //#endregion
20826
- //#region src/weixin-bridge.ts
20827
- var weixin_bridge_exports = /* @__PURE__ */ __exportAll({
20885
+ //#region src/im/accounts.ts
20886
+ const impls = /* @__PURE__ */ new Map();
20887
+ /** 注册账号实现(`index.ts` 装配时调用;重复 channel 覆盖——测试与热重载友好) */
20888
+ function registerImAccountImpl(impl) {
20889
+ impls.set(impl.channel, impl);
20890
+ }
20891
+ /** 取实现(未注册 ⇒ `null`,由调用方**响亮出声**,不要静默跳过) */
20892
+ function getImAccountImpl(channel) {
20893
+ return impls.get(channel) ?? null;
20894
+ }
20895
+ /** 已注册的实现名(诊断与错误文案用;顺序 = 注册顺序) */
20896
+ function imAccountChannels() {
20897
+ return [...impls.keys()];
20898
+ }
20899
+ /**
20900
+ * 账号行的渠道归一(**单一处**)。
20901
+ *
20902
+ * 缺省 `weixin`:老配置平移到 `im` 段时不必补这个字段——owner 的"统一配置"
20903
+ * 要能直接吃旧数据(与 `route.ts` 的 `normalizeImSection` 保持同一口径)。
20904
+ */
20905
+ function normalizeAccountChannel(channel) {
20906
+ const v = (channel ?? "").trim();
20907
+ return v === "" ? "weixin" : v;
20908
+ }
20909
+ //#endregion
20910
+ //#region src/im/weixin-account.ts
20911
+ /** 轮询间隔(getupdates 失败后重试;成功 = 立即续,长轮询本身 hold 35s) */
20912
+ const POLL_RETRY_MS = 3e3;
20913
+ /**
20914
+ * 微信的凭据就绪判据:**有 token** 才算绑定(baseUrl 留空 = 走官方默认)。
20915
+ * 与既有装配处逐字同义(原 `if (token === '') { …跳过… }`)。
20916
+ */
20917
+ function weixinCredentialReady(cred) {
20918
+ return typeof cred.token === "string" && cred.token.trim() !== "";
20919
+ }
20920
+ let deps = null;
20921
+ /** 装配时注入依赖(`bridge.ts` 在模块初始化末尾调用一次;见 `installWeixinAccountImpl`) */
20922
+ function setWeixinLoopDeps(d) {
20923
+ deps = d;
20924
+ }
20925
+ /** 单账号轮询循环(**函数体逐字照搬自 bridge.ts 的 `runAccountLoop`,仅改类型与依赖来源**) */
20926
+ async function runWeixinAccountLoop(input, loop) {
20927
+ if (!deps) {
20928
+ loop.lastError = "weixin 实现未注入依赖(装配顺序错误)";
20929
+ return;
20930
+ }
20931
+ const d = deps;
20932
+ const ctx = input.ctx;
20933
+ const cred = input.cred;
20934
+ let buf = "";
20935
+ while (!loop.stopped) {
20936
+ loop.lastPollAt = Date.now();
20937
+ try {
20938
+ const resp = await d.getUpdates({
20939
+ baseUrl: cred.baseUrl,
20940
+ token: cred.token,
20941
+ getUpdatesBuf: buf,
20942
+ timeoutMs: 35e3
20943
+ });
20944
+ buf = resp.get_updates_buf ?? buf;
20945
+ for (const msg of resp.msgs ?? []) {
20946
+ if (loop.stopped) break;
20947
+ if (msg.message_type !== 1) continue;
20948
+ if (input.machine) await d.handleMachineIncoming(ctx, input.accountId, cred, msg);
20949
+ else await d.handleIncoming(ctx, input.root, input.accountId, cred, msg);
20950
+ }
20951
+ } catch (err) {
20952
+ loop.lastError = err instanceof Error ? err.message : String(err);
20953
+ await new Promise((r) => setTimeout(r, POLL_RETRY_MS));
20954
+ }
20955
+ }
20956
+ }
20957
+ /** 微信账号实现(登记的 channel 名 = `weixin`;与配置里的取值一致) */
20958
+ const weixinAccountImpl = {
20959
+ channel: "weixin",
20960
+ isReady: weixinCredentialReady,
20961
+ notReadyReason: () => "**无 token**(未绑定)",
20962
+ startLoop(input) {
20963
+ const loop = {
20964
+ stopped: false,
20965
+ lastPollAt: 0
20966
+ };
20967
+ runWeixinAccountLoop(input, loop);
20968
+ return loop;
20969
+ }
20970
+ };
20971
+ /**
20972
+ * 注册微信实现**并注入依赖**(`bridge.ts` 调用;幂等——重复注册覆盖同名 channel)。
20973
+ *
20974
+ * ⚠️ 必须在首次 `syncHumanChannel` / `syncCccBridge` **之前**发生。
20975
+ * `bridge.ts` 在**模块求值时**调用它一次(不是等到 `registerWeixinBridge`)——
20976
+ * 因为测试与面板会**直接**调 `syncCccBridge`,那时装配入口还没跑过;
20977
+ * 若依赖装配入口注册,那些路径会全部走进"未实现"分支(本轮实测:12 例红,根因就是它)。
20978
+ */
20979
+ function installWeixinAccountImpl(d) {
20980
+ setWeixinLoopDeps(d);
20981
+ registerImAccountImpl(weixinAccountImpl);
20982
+ }
20983
+ //#endregion
20984
+ //#region src/im/bridge.ts
20985
+ var bridge_exports = /* @__PURE__ */ __exportAll({
20828
20986
  handleIncoming: () => handleIncoming,
20829
20987
  handleMachineIncoming: () => handleMachineIncoming,
20830
20988
  manualOutputFallbackNeeded: () => manualOutputFallbackNeeded,
@@ -20843,14 +21001,12 @@ const bridges = /* @__PURE__ */ new Map();
20843
21001
  *
20844
21002
  * 机器级 poller **不属于任何 CCC**(这正是"上移 ACC 层"的含义:谁轮询不再由 CCC 数量决定)。
20845
21003
  * 用途有二:① `runAccountLoop` 的 `root` 形参在机器级模式下不被使用,传它以示"无根";
20846
- * ② 归属表(`human-channel.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
21004
+ * ② 归属表(`im/inbound.ts`)按 root 区分拥有者 ⇒ 机器级用这个哨兵值,
20847
21005
  * 与任何**真实** CCC 根都不会撞(真根一律是绝对路径)。
20848
21006
  */
20849
21007
  const MACHINE_SCOPE = "<machine>";
20850
21008
  /** 机器级 poller(账号 id → 循环)。与 `bridges` **互斥**:同一时刻只有一种模式在跑。 */
20851
21009
  const machineLoops = /* @__PURE__ */ new Map();
20852
- /** 轮询间隔(getupdates 失败后重试延迟;成功 = 立即续轮询) */
20853
- const POLL_RETRY_MS = 3e3;
20854
21010
  /** 媒体大小上限(M6:20MB,对齐 vlm-describe;超限降级告知不落盘) */
20855
21011
  const MEDIA_MAX_BYTES = 20971520;
20856
21012
  /** typing_ticket 缓存(每 (accountId, fromUserId);getconfig 一次后续复用——对齐参考实现 typingTicketCache) */
@@ -20901,31 +21057,16 @@ async function sendTypingStop(cred, accountId, fromUserId) {
20901
21057
  console.log(`[serenity-hooks] weixin-bridge typing stop skipped: ${err instanceof Error ? err.message : String(err)}`);
20902
21058
  }
20903
21059
  }
20904
- /** 单账号轮询循环:getupdates 长轮询 → 逐消息分发 → 回复回写 */
20905
- async function runAccountLoop(ctx, root, accountId, cred, loop, machine = false) {
20906
- let buf = "";
20907
- while (!loop.stopped) {
20908
- loop.lastPollAt = Date.now();
20909
- try {
20910
- const resp = await getUpdates({
20911
- baseUrl: cred.baseUrl,
20912
- token: cred.token,
20913
- getUpdatesBuf: buf,
20914
- timeoutMs: 35e3
20915
- });
20916
- buf = resp.get_updates_buf ?? buf;
20917
- for (const msg of resp.msgs ?? []) {
20918
- if (loop.stopped) break;
20919
- if (msg.message_type !== 1) continue;
20920
- if (machine) await handleMachineIncoming(ctx, accountId, cred, msg);
20921
- else await handleIncoming(ctx, root, accountId, cred, msg);
20922
- }
20923
- } catch (err) {
20924
- loop.lastError = err instanceof Error ? err.message : String(err);
20925
- await new Promise((r) => setTimeout(r, POLL_RETRY_MS));
20926
- }
20927
- }
20928
- }
21060
+ /**
21061
+ * 🔴 **2026-10-19(P2)`runAccountLoop` 已迁出本文件** —— 逐字搬到
21062
+ * `im/weixin-account.ts`,成为 `ImAccountImpl` 的一个实现(微信)。
21063
+ *
21064
+ * 为什么搬:装配处原先把渠道写死在这里(`channel !== 'weixin'` ⇒ 跳过)。
21065
+ * owner 定向「飞书和微信一样,只不过是 account 方实现不同」⇒ 起循环这个动作
21066
+ * 归**账号自己的实现**,装配处只按 `channel` 查派发表。
21067
+ * ⚠️ 依赖(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)通过
21068
+ * `installWeixinAccountImpl` **注入**,避免 `bridge ⇄ weixin-account` 互相 import 成环。
21069
+ */
20929
21070
  /**
20930
21071
  * 手动输出模式的**机制标记**(v1.30.10 引入;v1.30.16 按 S142 用户"这个词不能让 ACC 定义,
20931
21072
  * 要让 CCC 定义"重构)。
@@ -20970,7 +21111,7 @@ function manualOutputFallbackNeeded(input) {
20970
21111
  */
20971
21112
  async function resolveCccRootByAlias(ctx, alias) {
20972
21113
  try {
20973
- const { listCccs } = await import("./ccc-roots-RzasspMr.js").then((n) => n.a);
21114
+ const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
20974
21115
  const entries = await listCccs(ctx);
20975
21116
  const byName = entries.find((e) => e.name === alias);
20976
21117
  if (byName) return byName.root;
@@ -20989,7 +21130,7 @@ async function resolveCccRootByAlias(ctx, alias) {
20989
21130
  * 三档(方案 §3.1,**刻意分档**):
20990
21131
  * - **未命中** ⇒ 不投递 + **留痕**(`console.warn`,静默档:这是"表里没有你",不是错误)
20991
21132
  * - **表残缺** ⇒ **响亮出声**(`console.error`;且**独立于是否命中**)
20992
- * - **目标不存在**(CCC / 角色)⇒ **响亮报错**(`console.error`;不静默丢弃)
21133
+ * - **目标不存在**(CCC / **轨迹**)⇒ **响亮报错**(`console.error`;不静默丢弃)
20993
21134
  *
20994
21135
  * @param input.cccFallback 过渡期回退(方案 Q5);**机器级模式传 `null`** ⇒ 未命中即不投递
20995
21136
  */
@@ -21000,7 +21141,7 @@ async function resolveInboundTarget(ctx, input) {
21000
21141
  user: input.user,
21001
21142
  fallback: input.cccFallback ? {
21002
21143
  ccc: input.cccFallback.root,
21003
- role: input.cccFallback.role
21144
+ role: input.cccFallback.trajectory
21004
21145
  } : null
21005
21146
  });
21006
21147
  for (const defect of route.defects) {
@@ -21040,10 +21181,10 @@ async function resolveInboundTarget(ctx, input) {
21040
21181
  });
21041
21182
  return null;
21042
21183
  }
21043
- const role = readSkiffRoles(root).get(target.role);
21044
- if (!role) {
21045
- const detail = `目标 CCC "${target.ccc}" 未定义 skiff 角色 "${target.role}"`;
21046
- console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表或该 CCC 的 skiff.roles`);
21184
+ const { findDirNameByCode } = await import("./profile-DyMJ_bfr.js").then((n) => n.i);
21185
+ if (findDirNameByCode(root, target.role) === null) {
21186
+ const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
21187
+ console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
21047
21188
  appendHumanChannelAudit({
21048
21189
  kind: "inbound-target-missing",
21049
21190
  account: input.account,
@@ -21054,10 +21195,11 @@ async function resolveInboundTarget(ctx, input) {
21054
21195
  });
21055
21196
  return null;
21056
21197
  }
21198
+ const profile = readSkiffProfileByCode(root, target.role);
21057
21199
  return {
21058
21200
  root,
21059
- roleName: target.role,
21060
- role
21201
+ trajectory: target.role,
21202
+ profile
21061
21203
  };
21062
21204
  }
21063
21205
  /**
@@ -21093,18 +21235,18 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21093
21235
  if (preset) resolved = preset.target;
21094
21236
  else {
21095
21237
  if (!settings.enabled) return;
21096
- const cccRole = matchWeixinRoute(settings.routes ?? [], fromUserId);
21238
+ const cccTarget = matchWeixinRoute(settings.routes ?? [], fromUserId);
21097
21239
  resolved = await resolveInboundTarget(ctx, {
21098
21240
  account: accountId,
21099
21241
  user: fromUserId,
21100
- cccFallback: cccRole ? {
21242
+ cccFallback: cccTarget ? {
21101
21243
  root,
21102
- role: cccRole
21244
+ trajectory: cccTarget
21103
21245
  } : null
21104
21246
  });
21105
21247
  }
21106
21248
  if (!resolved) return;
21107
- const { roleName, role } = resolved;
21249
+ const { trajectory, profile } = resolved;
21108
21250
  root = resolved.root;
21109
21251
  if (!text && mediaRefs.length === 0) {
21110
21252
  if (hasVoiceItem(msg)) await sendTextMessage({
@@ -21117,19 +21259,25 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21117
21259
  return;
21118
21260
  }
21119
21261
  try {
21120
- const sessionId = weixinSessionIdFor(fromUserId);
21262
+ const sessionId = weixinSessionIdForTrajectory(trajectory);
21121
21263
  const existing = getSkiffAgent(sessionId);
21122
21264
  const hc = readCheapWorkerConfig(root);
21123
- if (existing) try {
21124
- ensureSkiffSession(root, existing, roleName, role, true);
21125
- } catch (err) {
21126
- console.warn(`[serenity-hooks] weixin-bridge ensure 失败(不影响处理): ${String(err?.message ?? err)}`);
21127
- }
21128
21265
  const ref = existing ? {
21129
21266
  agent: existing,
21130
21267
  sessionId,
21131
21268
  resumed: true
21132
- } : await createSkiffAgent(ctx, root, roleName, role, hc?.defaultModel, sessionId);
21269
+ } : await createSkiffAgent(ctx, root, profile ?? {
21270
+ dirName: trajectory,
21271
+ code: trajectory,
21272
+ path: "",
21273
+ tools: [],
21274
+ msms: [],
21275
+ cognitiveTrajectory: {
21276
+ session: true,
21277
+ keeper: true,
21278
+ rebuild: true
21279
+ }
21280
+ }, hc?.defaultModel, sessionId);
21133
21281
  if (!ref.resumed) await sendTextMessage({
21134
21282
  baseUrl: cred.baseUrl,
21135
21283
  token: cred.token,
@@ -21186,9 +21334,9 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21186
21334
  }
21187
21335
  }
21188
21336
  const parts = [];
21189
- const activeWorkspace = getActiveSessionInfo(sessionId);
21190
- if (activeWorkspace?.mdPath) {
21191
- if (!ensureWorkspacePromptSection(ref.agent, sessionId)) parts.push(workspaceCognitiveTrajectoryLine(activeWorkspace.mdPath));
21337
+ const workspaceMd = skiffWorkspaceMdPath(root, sessionId) ?? getActiveSessionInfo(sessionId)?.mdPath ?? null;
21338
+ if (workspaceMd) {
21339
+ if (!ensureWorkspacePromptSection(ref.agent, sessionId, root)) parts.push(workspaceCognitiveTrajectoryLine(workspaceMd));
21192
21340
  }
21193
21341
  const manualOutput = settings.autoReplyWithLastMessage === false;
21194
21342
  if (text) parts.push(text);
@@ -21199,7 +21347,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21199
21347
  root,
21200
21348
  accountId,
21201
21349
  userId: fromUserId,
21202
- role: roleName
21350
+ role: trajectory
21203
21351
  });
21204
21352
  parts.push(weixinManualOutputMarker(root, accountId, fromUserId));
21205
21353
  } else forgetManualOutputSession(sessionId);
@@ -21210,13 +21358,13 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21210
21358
  accountId,
21211
21359
  userId: fromUserId,
21212
21360
  sessionId,
21213
- role: roleName,
21361
+ role: trajectory,
21214
21362
  text,
21215
21363
  media: hookMedia
21216
21364
  })).catch((err) => {
21217
21365
  console.log(`[serenity-hooks] weixin hook incoming error: ${err instanceof Error ? err.message : String(err)}`);
21218
21366
  });
21219
- const answer = (await askSkiff(ctx, ref.agent, question, void 0, { includeCognitiveTrajectory: false })).answer ?? "";
21367
+ const answer = (await askSkiff(ctx, ref.agent, question)).answer ?? "";
21220
21368
  if (manualOutput) {
21221
21369
  if (!manualOutputFallbackNeeded({
21222
21370
  fallbackOnNoSend: settings.fallbackOnNoSend,
@@ -21232,13 +21380,13 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21232
21380
  text: reply,
21233
21381
  contextToken: msg.context_token
21234
21382
  });
21235
- console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, role=${roleName})`);
21383
+ console.warn(`[serenity-hooks] ⚠ weixin 输出兜底:agent 本轮未发送(闸门打回用尽)→ 桥转发最终文本(session=${sessionId}, trajectory=${trajectory})`);
21236
21384
  if (hookRel) invokeWeixinHook(root, hookRel, buildOutgoingHookEvent({
21237
21385
  cccRoot: root,
21238
21386
  accountId,
21239
21387
  userId: fromUserId,
21240
21388
  sessionId,
21241
- role: roleName,
21389
+ role: trajectory,
21242
21390
  reply,
21243
21391
  source: "reply-fallback"
21244
21392
  })).catch((err) => {
@@ -21260,7 +21408,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21260
21408
  accountId,
21261
21409
  userId: fromUserId,
21262
21410
  sessionId,
21263
- role: roleName,
21411
+ role: trajectory,
21264
21412
  reply
21265
21413
  })).catch((err) => {
21266
21414
  console.log(`[serenity-hooks] weixin hook outgoing error: ${err instanceof Error ? err.message : String(err)}`);
@@ -21305,7 +21453,7 @@ async function handleMachineIncoming(ctx, accountId, cred, msg) {
21305
21453
  * 已存在(配置变化热重建)→ 先停旧循环再启动新的。
21306
21454
  *
21307
21455
  * 🔴 **P0-2(2026-09-27,方案 §2.6):一账号一 poller** —— 起循环**之前**先过机器级归属判定
21308
- * (`human-channel.ts`):身份被**别的** CCC 占着的账号**不起循环**,只留一条响亮日志。
21456
+ * (`im/inbound.ts`):身份被**别的** CCC 占着的账号**不起循环**,只留一条响亮日志。
21309
21457
  * 身份 = **凭据 token 的哈希**(不是 `accountId`:那是每 CCC 的本地标签,跨 CCC 无意义)。
21310
21458
  * 先 `releasePollersOf(root)` 再重新 claim —— 热重建语义与"归属粘住不漂"的论证见该函数注释。
21311
21459
  */
@@ -21320,10 +21468,23 @@ function syncCccBridge(ctx, root) {
21320
21468
  if (!settings.enabled) return;
21321
21469
  const requests = [];
21322
21470
  const creds = /* @__PURE__ */ new Map();
21471
+ /** 账号 id → 它的实现(P2:按 `channel` 查派发表) */
21472
+ const implByAccount = /* @__PURE__ */ new Map();
21323
21473
  for (const account of settings.accounts ?? []) {
21324
21474
  if (account.enabled === false) continue;
21475
+ const channel = normalizeAccountChannel(account.channel);
21476
+ const impl = getImAccountImpl(channel);
21477
+ if (!impl) {
21478
+ if (!warnedChannels.has(`${account.accountId}:${channel}`)) {
21479
+ warnedChannels.add(`${account.accountId}:${channel}`);
21480
+ console.warn(`[serenity-hooks] ⚠ weixin-bridge: 账号 ${account.accountId}(ccc=${root})的渠道 "${channel}" **本机未实现** ⇒ 不起循环(已实现:${imAccountChannels().join(", ") || "(无)"})`);
21481
+ }
21482
+ continue;
21483
+ }
21325
21484
  const cred = readWeixinCredential(root, account.accountId);
21326
21485
  if (!cred) continue;
21486
+ if (!impl.isReady(cred)) continue;
21487
+ implByAccount.set(account.accountId, impl);
21327
21488
  creds.set(account.accountId, cred);
21328
21489
  requests.push({
21329
21490
  root,
@@ -21339,13 +21500,16 @@ function syncCccBridge(ctx, root) {
21339
21500
  };
21340
21501
  for (const claim of plan.claims) {
21341
21502
  const cred = creds.get(claim.accountId);
21342
- if (!cred) continue;
21343
- const loop = {
21344
- stopped: false,
21345
- lastPollAt: 0
21346
- };
21503
+ const impl = implByAccount.get(claim.accountId);
21504
+ if (!cred || !impl) continue;
21505
+ const loop = impl.startLoop({
21506
+ ctx,
21507
+ root,
21508
+ accountId: claim.accountId,
21509
+ cred,
21510
+ machine: false
21511
+ });
21347
21512
  bridge.loops.set(claim.accountId, loop);
21348
- runAccountLoop(ctx, root, claim.accountId, cred, loop);
21349
21513
  }
21350
21514
  commitPollerClaims(plan);
21351
21515
  if (bridge.loops.size > 0) {
@@ -21396,6 +21560,8 @@ function syncMachinePollers(ctx, hc) {
21396
21560
  stopCccBridges();
21397
21561
  const requests = [];
21398
21562
  const creds = /* @__PURE__ */ new Map();
21563
+ /** 账号 id → 它的实现(P2:**按 `channel` 查派发表**,不再写死 weixin) */
21564
+ const implByAccount = /* @__PURE__ */ new Map();
21399
21565
  for (const account of hc.accounts) {
21400
21566
  const id = account.id;
21401
21567
  if (account.enabled === false) continue;
@@ -21403,23 +21569,27 @@ function syncMachinePollers(ctx, hc) {
21403
21569
  console.warn("[serenity-hooks] ⚠ human-channel: 机器级账号表有一行**缺 `id`** ⇒ 跳过(该行无法被订阅表引用)");
21404
21570
  continue;
21405
21571
  }
21406
- if (account.channel !== "weixin") {
21407
- if (!warnedChannels.has(`${id}:${account.channel ?? ""}`)) {
21408
- warnedChannels.add(`${id}:${account.channel ?? ""}`);
21409
- console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} 的渠道 "${account.channel ?? "(未填)"}" **本机未实现** ⇒ 不起 poller(当前只有 weixin)`);
21572
+ const channel = normalizeAccountChannel(account.channel);
21573
+ const impl = getImAccountImpl(channel);
21574
+ if (!impl) {
21575
+ if (!warnedChannels.has(`${id}:${channel}`)) {
21576
+ warnedChannels.add(`${id}:${channel}`);
21577
+ console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} 的渠道 "${account.channel ?? "(未填)"}" **本机未实现** ⇒ 不起 poller(已实现:${imAccountChannels().join(", ") || "(无)"})`);
21410
21578
  }
21411
21579
  continue;
21412
21580
  }
21413
21581
  const token = (account.token ?? "").trim();
21414
- if (token === "") {
21415
- console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} **无 token**(未绑定)⇒ 不起 poller`);
21416
- continue;
21417
- }
21418
- creds.set(id, {
21582
+ const cred = {
21419
21583
  token,
21420
21584
  baseUrl: "",
21421
21585
  userId: account.userId
21422
- });
21586
+ };
21587
+ if (!impl.isReady(cred)) {
21588
+ console.warn(`[serenity-hooks] ⚠ human-channel: 账号 ${id} ${impl.notReadyReason?.(cred) ?? "**凭据不全**"} ⇒ 不起 poller`);
21589
+ continue;
21590
+ }
21591
+ implByAccount.set(id, impl);
21592
+ creds.set(id, cred);
21423
21593
  requests.push({
21424
21594
  root: MACHINE_SCOPE,
21425
21595
  accountId: id,
@@ -21431,13 +21601,16 @@ function syncMachinePollers(ctx, hc) {
21431
21601
  stopMachineLoops();
21432
21602
  for (const claim of plan.claims) {
21433
21603
  const cred = creds.get(claim.accountId);
21434
- if (!cred) continue;
21435
- const loop = {
21436
- stopped: false,
21437
- lastPollAt: 0
21438
- };
21604
+ const impl = implByAccount.get(claim.accountId);
21605
+ if (!cred || !impl) continue;
21606
+ const loop = impl.startLoop({
21607
+ ctx,
21608
+ root: MACHINE_SCOPE,
21609
+ accountId: claim.accountId,
21610
+ cred,
21611
+ machine: true
21612
+ });
21439
21613
  machineLoops.set(claim.accountId, loop);
21440
- runAccountLoop(ctx, MACHINE_SCOPE, claim.accountId, cred, loop, true);
21441
21614
  }
21442
21615
  commitPollerClaims(plan);
21443
21616
  if (machineLoops.size > 0) console.log(`[serenity-hooks] ✓ human-channel: 机器级 poller accounts=${[...machineLoops.keys()].join(",")}(与 CCC 无关)`);
@@ -21469,7 +21642,7 @@ function syncHumanChannel(ctx) {
21469
21642
  (async () => {
21470
21643
  try {
21471
21644
  stopMachineLoops();
21472
- const { listCccs } = await import("./ccc-roots-RzasspMr.js").then((n) => n.a);
21645
+ const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
21473
21646
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
21474
21647
  } catch {}
21475
21648
  })();
@@ -21521,8 +21694,8 @@ async function sendProactiveText(input) {
21521
21694
  const account = resolveWeixinAccount(root, input.accountId);
21522
21695
  if (!account.ok) return account;
21523
21696
  const { accountId, cred } = account;
21524
- const sessionId = weixinSessionIdFor(toUserId);
21525
- const role = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
21697
+ const trajectory = matchWeixinRoute(settings.routes ?? [], toUserId) ?? "";
21698
+ const sessionId = weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(toUserId) || "unknown");
21526
21699
  try {
21527
21700
  await sendTextMessage({
21528
21701
  baseUrl: cred.baseUrl,
@@ -21542,7 +21715,7 @@ async function sendProactiveText(input) {
21542
21715
  accountId,
21543
21716
  userId: toUserId,
21544
21717
  sessionId,
21545
- role,
21718
+ role: trajectory,
21546
21719
  reply: markdownToPlainText(input.text),
21547
21720
  source: "proactive"
21548
21721
  })).catch((err) => {
@@ -21553,7 +21726,7 @@ async function sendProactiveText(input) {
21553
21726
  accountId,
21554
21727
  userId: toUserId,
21555
21728
  sessionId,
21556
- role
21729
+ role: trajectory
21557
21730
  };
21558
21731
  }
21559
21732
  /**
@@ -21589,15 +21762,35 @@ function registerWeixinBridge(ctx) {
21589
21762
  ctx.on("session/created", () => syncHumanChannel(ctx));
21590
21763
  } catch {}
21591
21764
  }
21765
+ /**
21766
+ * 🔴 **P2 装配:微信实现登记进"账号实现派发表"**(模块求值时执行,不是等 `apply`)。
21767
+ *
21768
+ * ## 为什么必须在模块求值时做(本轮实测教训)
21769
+ *
21770
+ * 首版把这句放在 `registerWeixinBridge` 里 ⇒ 全量测试 **12 例红**,报的全是
21771
+ * 「`weixinBridgeStatus()` 空数组」。根因:测试与设置面板会**直接**调 `syncCccBridge` /
21772
+ * `syncHumanChannel`,那时装配入口还没跑过 ⇒ 派发表是空的 ⇒ 每条账号都走进
21773
+ * 「渠道未实现」分支(那分支是给**真正**未实现的渠道用的)。
21774
+ *
21775
+ * ⇒ 判据:**"谁能被调"决定"注册要在哪儿发生"**,不能按"装配流程"想当然。
21776
+ *
21777
+ * 注入的是本模块自己的函数(`getUpdates` / `handleIncoming` / `handleMachineIncoming`)——
21778
+ * 不需要 `weixin-account.ts import 回 bridge.ts`,避免两个文件互相 import 成环。
21779
+ */
21780
+ installWeixinAccountImpl({
21781
+ getUpdates,
21782
+ handleIncoming,
21783
+ handleMachineIncoming
21784
+ });
21592
21785
  //#endregion
21593
- //#region src/weixin-send-api.ts
21786
+ //#region src/im/send.ts
21594
21787
  /** 请求体上限(文本消息远小于此;防滥用) */
21595
21788
  const MAX_BODY_BYTES = 65536;
21596
21789
  /** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
21597
21790
  const WEIXIN_SEND_MAX_TEXT = 4e3;
21598
21791
  /**
21599
21792
  * 面名(C4 块 A:listener 生命周期归 `face-host`,面向宿主提供规格)。
21600
- * 本面=面 C(端口语义见 `ports.ts` 的 `FACE_PORTS.weixinSend`)。
21793
+ * 本面=面 C(端口语义见 `kernel/ports.ts` 的 `FACE_PORTS.weixinSend`)。
21601
21794
  */
21602
21795
  const FACE_WEIXIN_SEND = "weixin-send-api";
21603
21796
  /** 监听地址:**只绑 loopback**(本面不变量——不经网关、公网不可达,故不做共享密钥) */
@@ -21705,7 +21898,7 @@ function matchCcc(input, candidates) {
21705
21898
  }
21706
21899
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
21707
21900
  async function collectCandidates(ctx) {
21708
- const { listCccs } = await import("./ccc-roots-RzasspMr.js").then((n) => n.a);
21901
+ const { listCccs } = await import("./ccc-roots-jZYR2GVE.js").then((n) => n.a);
21709
21902
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
21710
21903
  const seen = /* @__PURE__ */ new Set();
21711
21904
  const out = [];
@@ -21980,9 +22173,9 @@ function registerLifecycle(ctx) {
21980
22173
  registerDisposer(ctx, "self-started resources (gateway/weixin/skiff-debug faces + weixin bridges)", disposeAll);
21981
22174
  }
21982
22175
  //#endregion
21983
- //#region src/web-fetch-provider.ts
22176
+ //#region src/patches/web-fetch.ts
21984
22177
  /**
21985
- * web-fetch-provider.ts — fake-ip / TUN 网络下的 web_fetch provider(v1.30.12,S142)
22178
+ * patches/web-fetch.ts — fake-ip / TUN 网络下的 web_fetch provider(v1.30.12,S142)
21986
22179
  *
21987
22180
  * 问题(用户报告 + 实证):`web_fetch` 在本机恒失败,报
21988
22181
  * `URL hostname "X" resolves to a non-public IP address`(`WEB_BLOCKED_URL`)。
@@ -22171,7 +22364,7 @@ async function registerWebFetchProvider(ctx) {
22171
22364
  }
22172
22365
  }
22173
22366
  //#endregion
22174
- //#region src/opencode-provider.ts
22367
+ //#region src/patches/opencode-provider.ts
22175
22368
  /** `llm-pi-ai` 的 settings 命名空间(宿主 `dsh-llm-pi-ai` 的 `NS`) */
22176
22369
  const LLM_PI_AI_NAMESPACE = "llm-pi-ai";
22177
22370
  /**
@@ -22880,9 +23073,9 @@ function registerDeepseekVisionPatch(ctx, enabled) {
22880
23073
  registerDisposer(ctx, "deepseek vision patch retry timer", clearRetry);
22881
23074
  }
22882
23075
  //#endregion
22883
- //#region src/im-weixin.ts
23076
+ //#region src/im/weixin-channel.ts
22884
23077
  /**
22885
- * im-weixin.ts — 微信通道实现(`im-bridge` 的通道之一,v1.31.0)
23078
+ * im/weixin-channel.ts — 微信通道实现(`im-bridge` 的通道之一,v1.31.0)
22886
23079
  *
22887
23080
  * 能力归 ACC(S142 用户洞察):本文件吸收原 CCC MSM `.opencode/skills/home-serenity/scripts/
22888
23081
  * weixin-send.ts` 的 `send` / `send-file` / `users` 语义——协议、凭据、账号选择、别名解析、
@@ -22988,13 +23181,13 @@ const weixinChannel = {
22988
23181
  text: input.caption
22989
23182
  });
22990
23183
  if (settings.hook) {
22991
- const role = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
23184
+ const trajectory = matchWeixinRoute(settings.routes ?? [], input.userId) ?? "";
22992
23185
  invokeWeixinHook(input.root, settings.hook, buildOutgoingHookEvent({
22993
23186
  cccRoot: input.root,
22994
23187
  accountId: account.accountId,
22995
23188
  userId: input.userId,
22996
- sessionId: weixinSessionIdFor(input.userId),
22997
- role,
23189
+ sessionId: weixinSessionIdForTrajectory(trajectory !== "" ? trajectory : sanitizeFileName$1(input.userId) || "unknown"),
23190
+ role: trajectory,
22998
23191
  reply: input.caption ? `[文件] ${sent.fileName} — ${input.caption}` : `[文件] ${sent.fileName}`,
22999
23192
  source: "proactive",
23000
23193
  file: {
@@ -23024,7 +23217,7 @@ const weixinChannel = {
23024
23217
  })));
23025
23218
  const routes = (settings.routes ?? []).map((route) => ({
23026
23219
  user: route.user,
23027
- role: route.role
23220
+ role: route.trajectory
23028
23221
  }));
23029
23222
  return {
23030
23223
  enabled: settings.enabled === true,
@@ -23038,13 +23231,13 @@ const weixinChannel = {
23038
23231
  //#endregion
23039
23232
  //#region src/tools/im-bridge.ts
23040
23233
  /**
23041
- * im-bridge.ts(工具面)— IM 消息发送工具(v1.31.0)
23234
+ * im/channels.ts(工具面)— IM 消息发送工具(v1.31.0)
23042
23235
  *
23043
23236
  * 用户洞察(S142):微信桥是 ACC 提供的 → **发送能力也应是 ACC 的工具**;
23044
23237
  * 「当用户配置了微信桥则可用,不配置则不可见」。
23045
23238
  *
23046
23239
  * 本文件只做工具面三件事:① 从 agent 会话解析**本会话 CCC**(R4:不接受目标 CCC)
23047
- * ② 把参数交给 `im-bridge.ts`(能力层)分发 ③ 渲染结果/错误。
23240
+ * ② 把参数交给 `im/channels.ts`(能力层)分发 ③ 渲染结果/错误。
23048
23241
  *
23049
23242
  * 可见性不在这里判定:`seams/context.ts` 在会话就绪时按 CCC 配置
23050
23243
  * `agent.ctx.tools.restrict({ deny: ['im-bridge'] })` 把本工具从**未配置通道**的
@@ -23119,9 +23312,9 @@ function createImBridgeTool() {
23119
23312
  });
23120
23313
  }
23121
23314
  //#endregion
23122
- //#region src/live-sessions.ts
23315
+ //#region src/trajectory/live.ts
23123
23316
  /**
23124
- * live-sessions.ts — 进程内 live 会话取数 + 精简诊断
23317
+ * trajectory/live.ts — 进程内 live 会话取数 + 精简诊断
23125
23318
  *
23126
23319
  * ── 为什么单独成模块(R↓,2026-09-15 S142「ACC 侧 autopilot 退场」S1 搬迁)──
23127
23320
  * 这两个函数原先住在 `autopilot-trajectory.ts`,但它们的**主语不是 autopilot**:
@@ -23198,9 +23391,9 @@ function diagLive(ctx) {
23198
23391
  };
23199
23392
  }
23200
23393
  //#endregion
23201
- //#region src/container-status.ts
23394
+ //#region src/surface/observation.ts
23202
23395
  /**
23203
- * container-status.ts — 容器状态模型的**取数层唯一出口**(C5「观察面归一」,S142 2026-09-15)
23396
+ * surface/observation.ts — 容器状态模型的**取数层唯一出口**(C5「观察面归一」,S142 2026-09-15)
23204
23397
  *
23205
23398
  * 为什么存在(R↓):ACC 有多个观察面(`dashboard health` / `acc-diag` / WebUI 面板 /
23206
23399
  * 系统提示词注入),它们读的是同一批宿主事实。此前"这些事实怎么取"写在**每个消费方各自的
@@ -23218,15 +23411,15 @@ function diagLive(ctx) {
23218
23411
  * 依赖分层(为什么本模块"重"而 `kit-ops` / `status` 保持"轻"):
23219
23412
  * {@link containerClocks} 必须读 `wake-scheduler` 的进程内快照,而该模块带 **@deepseek-ai 值依赖**
23220
23413
  * (`dsh-llm` 的 `createUserMessage`)。故本模块**只能**被本就重型或异步的消费方使用:
23221
- * `diag-ops`(静态 import)、`api.ts` / `kit-ops`(`await import(...)`,与 `api.ts` 既有的
23414
+ * `diag-ops`(静态 import)、`surface/http.ts` / `kit-ops`(`await import(...)`,与 `surface/http.ts` 既有的
23222
23415
  * "保持静态链纯净"约定一致)。**轻链模块不得静态 import 本模块**。
23223
23416
  *
23224
23417
  * 已有的单一真相源继续指向原处(**不复制**):
23225
- * L0 根解析 `ccc.ts findSerenityRoot`|L1/L2 根与枚举 `ccc-roots.ts`|宿主契约 `host/contract.ts`
23226
- * |CCC 配置 `ccc.ts loadSerenityConfig`|注册表 `msm-ops.ts`|时钟 `wake-scheduler.ts`
23227
- * |唤醒注册表 `wake-registry.ts`。
23418
+ * L0 根解析 `kernel/ccc.ts findSerenityRoot`|L1/L2 根与枚举 `kernel/ccc-roots.ts`|宿主契约 `host/contract.ts`
23419
+ * |CCC 配置 `kernel/ccc.ts loadSerenityConfig`|注册表 `tools/msm/service.ts`|时钟 `wake/scheduler.ts`
23420
+ * |唤醒注册表 `wake/registry.ts`。
23228
23421
  */
23229
- var container_status_exports = /* @__PURE__ */ __exportAll({
23422
+ var observation_exports = /* @__PURE__ */ __exportAll({
23230
23423
  checkRegistryHealth: () => checkRegistryHealth,
23231
23424
  checkRegistryQuality: () => checkRegistryQuality,
23232
23425
  containerClocks: () => containerClocks,
@@ -23247,7 +23440,7 @@ function containerIdentity(root) {
23247
23440
  }
23248
23441
  /**
23249
23442
  * 三原则取数。
23250
- * ⚠️ 与 `ccc.ts` 的 P1/P2 原语是**同一实现**(`existsSync(.serenity)` / `findGitRoot`),
23443
+ * ⚠️ 与 `kernel/ccc.ts` 的 P1/P2 原语是**同一实现**(`existsSync(.serenity)` / `findGitRoot`),
23251
23444
  * 本函数只把它们组装成"一次检查"的形状;判定语义未变(P1 要求文件非空、P2 要求 `.git` 存在)。
23252
23445
  */
23253
23446
  function containerPrinciples(root) {
@@ -23271,7 +23464,7 @@ function containerPrinciples(root) {
23271
23464
  };
23272
23465
  }
23273
23466
  /**
23274
- * **判据 A —— 注册表「结构完整性」**(原 `kit-ops.ts checkRegistryHealth`,C5 迁入本模块)。
23467
+ * **判据 A —— 注册表「结构完整性」**(原 `tools/kit/service.ts checkRegistryHealth`,C5 迁入本模块)。
23275
23468
  *
23276
23469
  * 回答的问题:**这张表坏没坏**——文件能不能被安全解析(JSON 合法 / 顶层形状 / entry 字段类型 /
23277
23470
  * name 全局唯一 / path 在根内且脚本存在)。
@@ -23366,7 +23559,7 @@ function checkRegistryHealth(root) {
23366
23559
  * 时钟的进程内状态("为什么这轮没被唤起"的第一手判据)。
23367
23560
  *
23368
23561
  * 🔴 **只读快照,不重算**(设计红线):`armed`/`ticks`/`lastTickAt`/`lastSkipReason` 只存在于
23369
- * 调度器自己的模块级运行时对象里(`wake-scheduler.ts schedulerRuntime`)。任何"从配置或文件
23562
+ * 调度器自己的模块级运行时对象里(`wake/scheduler.ts schedulerRuntime`)。任何"从配置或文件
23370
23563
  * 重新推导"得到的值都会与真实调度器的状态不一致——那比没有可观测量更糟(会给出假的"一切正常")。
23371
23564
  */
23372
23565
  function containerClocks() {
@@ -23410,11 +23603,11 @@ function containerStatus(opts = {}) {
23410
23603
  };
23411
23604
  }
23412
23605
  //#endregion
23413
- //#region src/diag-ops.ts
23606
+ //#region src/tools/diag/service.ts
23414
23607
  /**
23415
- * diag-ops.ts — `acc-diag` 报告装配(v1.33,S142 §32.11;2026-09-15 段数收缩)
23608
+ * tools/diag/service.ts — `acc-diag` 报告装配(v1.33,S142 §32.11;2026-09-15 段数收缩)
23416
23609
  *
23417
- * 为什么单独成模块(R↓):工具面(`tools/acc-diag.ts`)只做"取 CCC 根 + 渲染",
23610
+ * 为什么单独成模块(R↓):工具面(`tools/diag/index.ts`)只做"取 CCC 根 + 渲染",
23418
23611
  * 装配逻辑放这里 ⇒ 纯函数可单测、不依赖 DSH 工具注册。
23419
23612
  *
23420
23613
  * 为什么**无动作参数**:诊断是低频动作(§32.9 收敛取向),一次调用即出全报告,
@@ -23426,7 +23619,7 @@ function containerStatus(opts = {}) {
23426
23619
  * ①b 唤醒时钟 —— `containerClocks().wake`(唤醒调度器进程内快照:武装态/计数/上次跳过原因)
23427
23620
  * ③ 唤醒注册表 —— `wake-registry.json` 的**在办条目**(state / at / target / lastResult)+ 补跑窗口
23428
23621
  * (🔴 2026-09-19 §0Q 甲案:终态条目**结案即清除** ⇒ 本段只列在办项,不再是"历史全量";
23429
- * 历史由 SESSION.md / 工具输出承担——该承诺原本就写在 `wake-registry.ts` 注释里,本次做实)
23622
+ * 历史由 SESSION.md / 工具输出承担——该承诺原本就写在 `wake/registry.ts` 注释里,本次做实)
23430
23623
  *
23431
23624
  * ── 2026-09-15:段数由四降为三(S142「ACC 侧 autopilot 退场」§1 二阶裁定)─────────────
23432
23625
  * ACC 侧 autopilot 整段退场后,报告里两段的**主语消失**,故删除(不是"暂时关掉"):
@@ -23442,7 +23635,7 @@ function containerStatus(opts = {}) {
23442
23635
  * 条目到期 / 补跑窗口 / 目标可解析 / CCC 闸)。**不得**以"顺手保留 ④"的方式把它偷偷带回。
23443
23636
  *
23444
23637
  * C5「观察面归一」(2026-09-15):① 走 `diagLive`(那是**live 运行态**的独有取数,没有第二个
23445
- * 来源);①b 时钟与 ③ 注册表经 `container-status.ts` 取(唯一取数出口)——**投影仍在本文件**
23638
+ * 来源);①b 时钟与 ③ 注册表经 `surface/observation.ts` 取(唯一取数出口)——**投影仍在本文件**
23446
23639
  * (诊断段不含 `message`,面板含;这是渲染差异,不是取数差异)。
23447
23640
  */
23448
23641
  /** 人读渲染一行时钟状态("为何没有 tick" 的第一手判据) */
@@ -23513,9 +23706,9 @@ function renderAccDiag(r) {
23513
23706
  return lines.join("\n");
23514
23707
  }
23515
23708
  //#endregion
23516
- //#region src/tools/acc-diag.ts
23709
+ //#region src/tools/diag/index.ts
23517
23710
  /**
23518
- * acc-diag.ts(工具面)— ACC 负责人专用的运行态诊断(v1.33,S142 §32.11)
23711
+ * tools/diag/index.ts(工具面)— ACC 负责人专用的运行态诊断(v1.33,S142 §32.11)
23519
23712
  *
23520
23713
  * 用户裁决(2026-09-14):「不要 diag-live 了;或者变成一个隐藏 tool **只对 home-serenity 可见**,
23521
23714
  * 做成我们专属的运行时诊断工具」。⇒ 合并原 `diag`(脚本侧条件链)+ `diag-live`(进程内运行态),
@@ -23535,7 +23728,7 @@ function renderAccDiag(r) {
23535
23728
  * (① live 运行态 / ①b 唤醒时钟 / ③ 唤醒注册表)。
23536
23729
  *
23537
23730
  * ⚠️ 2026-09-15(ACC 侧 autopilot 退场,S142):原四段降为三段——原 ②「面板解析」与
23538
- * ④「唤起条件链」的主语随机制消失,已删(见 `../diag-ops.ts` 文件头;④ 的缺口已登记)。
23731
+ * ④「唤起条件链」的主语随机制消失,已删(见 `tools/diag/service.ts` 文件头;④ 的缺口已登记)。
23539
23732
  */
23540
23733
  function createAccDiagTool(ctx) {
23541
23734
  return defineTool({
@@ -24809,7 +25002,7 @@ async function resolveSkiffRoot(ctx) {
24809
25002
  }
24810
25003
  }
24811
25004
  } catch {}
24812
- for (const r of liveRoots) if (readSkiffRoles(r).size > 0) return r;
25005
+ for (const r of liveRoots) if (listSkiffProfiles(r).length > 0) return r;
24813
25006
  const fromCwd = cccRootForCwd(process.cwd());
24814
25007
  if (fromCwd) return fromCwd;
24815
25008
  if (liveRoots[0]) return liveRoots[0];