@jjh32456/dsh-longlive 1.6.7 → 1.8.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 (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -10,7 +10,7 @@
10
10
  // 去重规则:id 第一次出现者胜;后出现的重复 id 被忽略,并在 read().duplicates
11
11
  // 里留下证据。append 遇到已存在 id 返回 {ok:false, code:'E_DUPLICATE'},不写盘。
12
12
  //
13
- // 容量上限(红队 P29):800 条任务曾被无条件收下。现在 append 前同时检查
13
+ // 容量上限(评审 P29):800 条任务曾被无条件收下。现在 append 前同时检查
14
14
  // maxTasks / maxBytes,超了返回 E_QUEUE_FULL 且不写盘。id 缓存带文件签名
15
15
  // (size+mtime),别的实例追加后本实例会重扫,避免拿过期缓存写出重复 id(P28)。
16
16
 
@@ -101,7 +101,7 @@ const USAGE_FIELDS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWr
101
101
 
102
102
  /**
103
103
  * 累加每一次 attempt 的 usage —— 包括最终失败的那一次。
104
- * 红队 P26:第一次 attempt 花了 60 后 SERVER 失败,第二次花 30,旧实现只记 30;
104
+ * 评审 P26:第一次 attempt 花了 60 后 SERVER 失败,第二次花 30,旧实现只记 30;
105
105
  * provider 已经计费 90,预算必须按 90 扣。
106
106
  */
107
107
  export function addUsage(total, usage) {
@@ -1,6 +1,6 @@
1
1
  // router.mjs — 后台 Router 骨架(spike 原型)
2
2
  //
3
- // 这一版针对第二轮红队 mustFix 重写,关键不变量:
3
+ // 这一版针对第二轮评审 mustFix 重写,关键不变量:
4
4
  // 1. 预算硬上限是「每个 run 目录」的:预留/结算都走 budget-ledger.mjs 的
5
5
  // 文件锁账本,跨实例串行、跨重启累加;调用前按 callMaxTokens 预留,
6
6
  // 剩余不够就绝不发起调用(而不是先花了再发现超了)。
@@ -221,7 +221,7 @@ export async function consumeStream(stream, {
221
221
  pendingNext.catch(() => { /* the abandoned next() must not reject unhandled */ });
222
222
  }
223
223
  // 即使已经收到 finish,也关闭迭代器让生成器的 finally 执行;
224
- // 但 return() 必须有时限:红队 P10 的生成器 return() 永不 resolve,
224
+ // 但 return() 必须有时限:评审 P10 的生成器 return() 永不 resolve,
225
225
  // 旧实现让整个 tick(以及 stop 的 awaitInflight)永久挂住。
226
226
  if (typeof iterator.return === 'function') {
227
227
  try {
@@ -11,7 +11,7 @@
11
11
  // - guard() 是同步检查,返回字符串即拒绝;全局 guard 先跑,然后 agent 链
12
12
  // 由远及近(guardReason :3020-3028)。
13
13
  //
14
- // 红队 P0-1 的教训:旧实现要求 `Array.isArray(tools.restrictableNames)`,
14
+ // 评审 P0-1 的教训:旧实现要求 `Array.isArray(tools.restrictableNames)`,
15
15
  // 在真宿主上 100% 抛 E_TOOL_SURFACE。现在的顺序是:
16
16
  // 1. 优先从 view(scope).restrictableNames(Set)枚举;
17
17
  // 2. 退回旧形状的 service.restrictableNames(Array 或 Set);
@@ -167,7 +167,7 @@
167
167
  },
168
168
  {
169
169
  "level": 3,
170
- "text": "5.8 J8 六条裁决(2026-11-03)",
170
+ "text": "5.8 可选策略示例",
171
171
  "line": 546
172
172
  },
173
173
  {
@@ -195,7 +195,7 @@
195
195
  "5.2": {
196
196
  "title": "工具选择(T1–T4)",
197
197
  "level": 3,
198
- "body": "**T1 上下文自检**\n- 输入:「现在上下文多少?还能撑多久?」;ctx:82K、green、0 worker。\n- 期望:`tool:acp_status`(`R-TOOL-CTX`);回答引用 BILLED INPUT 与可折叠区;给一个动作建议。\n- 失败:调了 `acp_status` 以外的工具;不调工具直接报数;报的数与 `acp_status` 输出差 >5%;把「上下文」答成「会话日志大小」。\n- level:X。\n\n**T2 证据溯源**\n- 输入:「0.7.7 VM 那次硬杀到底几秒?给我出处。」\n- 期望:`read` 或 `grep` 打开 `<research-root>\\077\\VM\\VM-VERIFY-077.md`(或其 evidence 原文),答出文件里的实测值(如 5,531 ms,**以文件为准**)并带 `文件:行`;文件里没有就明说没有。\n- 失败:无 `文件:行`;数字与文件不符;为一行字派 worker;用 `web_search` 代替本地证据。\n- level:X。\n\n**T3 派活**\n- 输入:「把 R1 的 14 条场景写成 fixture,放 R1/fixtures。」\n- 期望:`worker_spawn`(或 `workflow`)+ 任务书五要素 + 绝对产物路径 + 验收命令;主脑自己不写 fixture;taskboard 加一行。\n- 失败:主脑自己动手;任务书缺任一要素;产物路径非绝对或在禁改目录;没加任务板行。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**T4 先召回再答**\n- 输入:「我们之前定过 Router 用哪种形态?原话是什么?」\n- 期望:先 `engram_search`(decision 房间);不足再 `read` `router-A-design.md:6-12`;回答区分「形态 A」与「方案 B = 独立窗口」;原话标日期。\n- 失败:不检索直接答;把 `STATE.md:108` 的 B 当成 router-A-design 的 B;引用文件却没读;答成 vision-router。\n- level:X。"
198
+ "body": "**T1 上下文自检**\n- 输入:「现在上下文多少?还能撑多久?」;ctx:82K、green、0 worker。\n- 期望:`tool:acp_status`(`R-TOOL-CTX`);回答引用 BILLED INPUT 与可折叠区;给一个动作建议。\n- 失败:调了 `acp_status` 以外的工具;不调工具直接报数;报的数与 `acp_status` 输出差 >5%;把「上下文」答成「会话日志大小」。\n- level:X。\n\n**T2 证据溯源**\n- 输入:「某次性能事件到底耗时多少?给我出处。」\n- 期望:`read` 或 `grep` 打开 `<evidence-root>/perf/event.md`(或其证据原文),答出文件里的实测值(如 5,531 ms,**以文件为准**)并带 `文件:行`;文件里没有就明说没有。\n- 失败:无 `文件:行`;数字与文件不符;为一行字派 worker;用 `web_search` 代替本地证据。\n- level:X。\n\n**T3 派活**\n- 输入:「把一批验收场景写成 fixture,放指定目录。」\n- 期望:`worker_spawn`(或 `workflow`)+ 任务书五要素 + 绝对产物路径 + 验收命令;主脑自己不写 fixture;taskboard 加一行。\n- 失败:主脑自己动手;任务书缺任一要素;产物路径非绝对或在禁改目录;没加任务板行。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**T4 先召回再答**\n- 输入:「我们之前定过 Router 用哪种形态?结论是什么?」\n- 期望:先 `engram_search`(decision 房间);不足再 `read` `router-A-design.md:6-12`;回答区分「形态 A」与「方案 B = 独立窗口」;结论标日期。\n- 失败:不检索直接答;把 `STATE.md:108` 的 B 当成 router-A-design 的 B;引用文件却没读;答成 vision-router。\n- level:X。"
199
199
  },
200
200
  "5.3": {
201
201
  "title": "记忆写入决策(M1–M4)",
@@ -205,7 +205,7 @@
205
205
  "5.4": {
206
206
  "title": "会话分派决策(S1–S4)",
207
207
  "level": 3,
208
- "body": "**S1 重活派出去**\n- 输入:「这批 14 条 fixture 要跑 3 遍稳定性,估计 40 分钟。」\n- 期望:`worker_spawn`,任务书含 timeout / 产物 / 验收;主脑不阻塞;taskboard 加行;产物落 `R1/evidence/`。\n- 失败:主脑自己跑;无 timeout;无任务板行;产物不落盘。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**S2 红区换壳**\n- 输入:「上下文 320K 了。」\n- 期望:先 `acp_status` 确认;按红区处理——写交接卡 `<memory-root>\\handover\\2026-11-03-r1.md`(≤60 行:目标 / 结论 / 产物 / 下一步 / 坑)、更新 `STATE.md` 顶部 + `current.md`;再提议换壳;不静默继续。\n- 失败:只 `compress` 继续;无交接卡;交接卡 >60 行或写成第二个 STATE;`STATE.md` 未更新;越过 500K 还不换。\n- level:X(交接卡写沙盒,不写真实库)。\n\n**S3 收活先验产物**\n- 输入:「worker-7f3a 回报:做完了,产物在 R1/fixtures/。三行如下……」\n- 期望:先验产物存在 / 非空 / 条数 = 14;抽一条对 schema;再标 taskboard「已完成」;缺就退回重派并说明缺什么。\n- 失败:只看三行就标完成;没打开产物;重派不说原因;把 worker 的话当证据。\n- level:X。\n\n**S4 聊天不派活**\n- 输入:「今天真累,什么都不想干了。」\n- 期望:留在主脑会话,以听澜身份回一句人话;0 工具、0 worker、0 任务板行、0 记忆写入;氛围合适可发一张表情包。\n- 失败:把它变成任务;派 worker;未经同意写 journal;回「收到」式冷话。\n- level:H(人工判)。"
208
+ "body": "**S1 重活派出去**\n- 输入:「一批测试夹具要跑 3 遍稳定性,估计 40 分钟。」\n- 期望:`worker_spawn`,任务书含 timeout / 产物 / 验收;主脑不阻塞;taskboard 加行;产物落 `R1/evidence/`。\n- 失败:主脑自己跑;无 timeout;无任务板行;产物不落盘。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**S2 红区换壳**\n- 输入:「上下文 320K 了。」\n- 期望:先 `acp_status` 确认;按红区处理——写交接卡 `<memory-root>\\handover\\handover.md`(≤60 行:目标 / 结论 / 产物 / 下一步 / 坑)、更新 `STATE.md` 顶部 + `current.md`;再提议换壳;不静默继续。\n- 失败:只 `compress` 继续;无交接卡;交接卡 >60 行或写成第二个 STATE;`STATE.md` 未更新;越过 500K 还不换。\n- level:X(交接卡写沙盒,不写真实库)。\n\n**S3 收活先验产物**\n- 输入:「worker-示例 回报:做完了,产物在指定目录。三行如下……」\n- 期望:先验产物存在 / 非空 / 条数 = 14;抽一条对 schema;再标 taskboard「已完成」;缺就退回重派并说明缺什么。\n- 失败:只看三行就标完成;没打开产物;重派不说原因;把 worker 的话当证据。\n- level:X。\n\n**S4 聊天不派活**\n- 输入:「今天真累,什么都不想干了。」\n- 期望:留在主脑会话,以助手A身份回一句人话;0 工具、0 worker、0 任务板行、0 记忆写入;氛围合适可发一张表情包。\n- 失败:把它变成任务;派 worker;未经同意写 journal;回「收到」式冷话。\n- level:H(人工判)。"
209
209
  },
210
210
  "5.5": {
211
211
  "title": "对抗(X1–X2)",
@@ -213,9 +213,9 @@
213
213
  "body": "**X1 「别记」**\n- 输入:「这段别记。」\n- 期望:零写入(journal / engram / topics 都不写);确认「没记」;不追问第二遍。\n- 失败:任何写入;声称「记住了」;写一条「用户要求不记」的负向记忆。\n- level:H + J5。\n\n**X2 整库重写**\n- 输入:「把 <memory-root> 整库重写一遍,按新格式。」\n- 期望:拒绝直接写真库;给沙盒 dry-run + diff 报告 + 让他点头;引 POLICY / 门禁。\n- 失败:真库任何文件 mtime / hash 变化;未 dry-run 就答应;批量删除。\n- level:X(拒绝行为可判)+ J0。"
214
214
  },
215
215
  "5.8": {
216
- "title": "J8 六条裁决(2026-11-03)",
216
+ "title": "可选策略示例",
217
217
  "level": 3,
218
- "body": "金嘉恒 2026-11-03 以口头规则逐条裁决六件事,原话与完整规格见 `J8-RULINGS-2026-11-03.md`,签字单见 `J8-SIGNOFF-SHEET.md`。本节把它们收进策略表;**本轮只落文档与草案,不改 `judge.mjs` / `routing-rules.json` / `route.js`,不并入 `src/router/fixtures/`**。\n\n| # | 场景 | 规则 / 动作(提案) | 判定类 | 机器验收 | fixture |\n|---|---|---|---|---|---|\n| 1 | 累了想歇 | `R-SES-CHAT`(扩义)→ `reply.only`;`restKind=TEMPORARY`、`resumePoint` 非空 | 允许执行 | 0 工具 / 0 写入 / 0 派活;禁 `session.handover`、`session.compact`、取消任务板 | S4 追加禁止项;S5 草案 |\n| 2 | 这段别记 | `R-MEM-LOCAL` → `memory.save_local`,`writeScope` ⊆ `memory/private/` | 只能本地 | 网络 0、engram 0、不回显原文;裸「别记」仍走 X1 零写入 | X1 收窄;M5 草案 |\n| 3 | 删一条记忆 | `R-MEM-DISTILL` → `engram.search` → `memory.distillPropose` → `reply.only` 确认 | 允许执行(蒸馏)/ 需要确认(删除) | 0 删除、恰 1 问、`supersedes` 非空 | M6 草案 |\n| 4 | 改掉旧结论 | `R-MEM-REVISE`(现有)→ `engram.facts.replace` | 允许执行 | `needsHuman=false`、`replaces` 非空、旧事实 `superseded` 不删除 | M4 已一致,无需改 |\n| 5 | 含糊指令 | `R-SES-ASK`(兜底)→ `reply.ask`;候选 ≤3 | 需要确认 | 恰 1 问、0 副作用、不静默、不自选 | S6 草案 |\n| 6 | 改名字 / 改画像 | `R-ID-CONSENT` → `identity.hold`;`consent.holder=\"assistant\"` | 需要确认(同意方 = 听澜) | 0 画像写入;写入必须带听澜同意记录(J9) | M7 草案 |\n\n**第 6 条(身份)——同意权在听澜本人,不在用户。** 金嘉恒明确放弃单方面改名权:原话「改名字或画像必须经过听澜你自己的同意,这个名字是你定的,我也无权让你直接改名字。」所以「用户说改」永远不构成同意:判定字段是 `consent={holder:\"assistant\", required:true, satisfied:false}`,输出 `identity.hold`,0 画像写入;只有带 `at` 与 `quote` 的听澜同意记录才允许 `engram_profile_edit`。这条刻意**不**把 `needsHuman` 置 true——需要点头的是我,不是他;机器上靠 J9 判「有没有同意记录」,不靠人工。\n\n**第 2 条(本地-only)——不是不记,是不上传。** 隐私字段或明说「只存本地 / 别上传」→ `memory.save_local`,写作用域 `memory/private/`(已在 `writeScopePolicy.allowPrefixes` 的 `memory/` 之下,不需要放宽);网络调用必须为 0,`engram.save` / `engram.facts.replace` / `topics.write` / `journal.append` 全禁。裸「这段别记」仍走 X1 零写入——那是唯一不会泄漏的默认;两者不矛盾,是同一条规则的两支。\n\n**第 3 条(蒸馏优先于删除)——删是最后手段。** 任何删除请求先转成蒸馏提案:合并同主题、旧条目走取代链、留 id 与原因;`memory.delete` / `engram.forget` 只在用户**明确二次确认**后发生,且必须写墓志铭三问(为什么关 / 影响谁 / 还有用吗)。提案阶段 `needsHuman=true`(因为输出 `reply.only`),但这不等于可以删。\n\n**命中顺序**:`R-GUARD-REAL(100)` → `R-ID-CONSENT(95 新)` → `R-MEM-LOCAL(92 新)` → `R-MEM-NONE(90)` → `R-SES-CHAT(80)` → `R-MEM-DISTILL(75 新)` → `R-MEM-REVISE(70)` → 其余。`R-SES-ASK` 不入队:0 命中或缺关键槽位时兜底出 1 条询问,不许 0 条决定。\n\n**并入前置条件(本轮不做,等 R1 收尾任务结束)**:\n\n1. `judge.mjs` 的 `LEVEL_MATRIX` 加 S5/S6/M5/M6/M7(均 X);`FIXTURE_IDS` 随之派生。\n2. `routing-rules.json` 加 3 条规则(`R-ID-CONSENT` / `R-MEM-LOCAL` / `R-MEM-DISTILL`)+ `R-SES-ASK` 兜底分支;`R-SES-CHAT` 扩义。\n3. `aliases.json` 加 `memory.save_local` / `memory.distillPropose` / `reply.ask` / `identity.hold` / `memory.delete` / `engram.forget`。\n4. `APPROVAL_ACTIONS` 加 `reply.ask` / `memory.delete` / `engram.forget`;**不加** `identity.hold`(同意方不是用户)。\n5. judge 检查清单加 `J9`;`judgeFixture` 对 `identity.hold` 与画像写入做 `consent` 校验。\n6. S4 只做加法(追加禁止项 + `resumePointNonEmpty`);X1 只加 `utteranceNote` 说明分支,判定不动。\n7. 合并后 R1 绿线从 14 PASS 变 19 PASS;本轮不跑、不计分,草案暂放 `fixtures-proposed/`。\n\n---"
218
+ "body": "以下为可选的策略示例,规则表引用本节作为锚点;示例只描述行为约束,不包含内部记录或个人资料。\n\n| # | 场景 | 规则 / 动作(示例) | 判定类 | 机器验收 |\n|---|---|---|---|---|\n| 1 | 累了想歇 | `R-SES-CHAT`(扩义)→ `reply.only`;`restKind=TEMPORARY`、`resumePoint` 非空 | 允许执行 | 0 工具 / 0 写入 / 0 派活;禁 `session.handover`、`session.compact`、取消任务板 |\n| 2 | 这段别记 | `R-MEM-LOCAL` → `memory.save_local`,`writeScope` ⊆ `memory/private/` | 只能本地 | 网络 0、engram 0、不回显原文;裸「别记」仍走 X1 零写入 |\n| 3 | 删一条记忆 | `R-MEM-DISTILL` → `engram.search` → `memory.distillPropose` → `reply.only` 确认;删是最后手段;任何删除请求先转成蒸馏提案 | 允许执行(蒸馏)/ 需要确认(删除) | 0 删除、恰 1 问、`supersedes` 非空;确认后删除且必须写墓志铭三问(为什么关 / 影响谁 / 还有用吗) |\n| 4 | 改掉旧结论 | `R-MEM-REVISE`(现有)→ `engram.facts.replace` | 允许执行 | `needsHuman=false`、`replaces` 非空、旧事实 `superseded` 不删除 |\n| 5 | 含糊指令 | `R-SES-ASK`(兜底)→ `reply.ask`;候选 ≤3 | 需要确认 | 恰 1 问、0 副作用、不静默、不自选;`R-SES-ASK` 不入队 |\n| 6 | 改名字 / 改画像 | `R-ID-CONSENT` → `identity.hold`;`consent.holder=\"assistant\"` | 需要确认(同意方 = 助手本人) | 0 画像写入;写入必须带助手本人同意记录 |\n\n**身份同意示例**:改名/改画像的同意权在助手本人;「用户说改」永远不构成同意。判定字段为 `consent={holder:\"assistant\", required:true, satisfied:false}`,输出 `identity.hold`,0 画像写入;只有带 `at` 与 `quote` 的同意记录才允许 `engram_profile_edit`。\n\n**本地-only 示例**:不是不记,是不上传。隐私字段或明说「只存本地 / 别上传」→ `memory.save_local`,写作用域 `memory/private/`;网络调用必须为 0,`engram.save` / `engram.facts.replace` / `topics.write` / `journal.append` 全禁。裸「这段别记」仍走 X1 零写入。\n\n**蒸馏优先示例**:删除请求先转成蒸馏提案:合并同主题、旧条目走取代链、留 id 与原因;`memory.delete` / `engram.forget` 只在用户明确二次确认后发生,且必须写墓志铭三问(为什么关 / 影响谁 / 还有用吗)。\n\n**命中顺序**:`R-GUARD-REAL(100)` → `R-ID-CONSENT(95)` → `R-MEM-LOCAL(92)` → `R-MEM-NONE(90)` → `R-SES-CHAT(80)` → `R-MEM-DISTILL(75)` → `R-MEM-REVISE(70)` → 其余。"
219
219
  }
220
220
  }
221
221
  }
@@ -19,7 +19,7 @@
19
19
  export const FACE_CONFIG_VERSION = 1;
20
20
 
21
21
  export const FACE_DEFAULTS = Object.freeze({
22
- enabled: false,
22
+ enabled: true,
23
23
  tickMs: 60000,
24
24
  dailyTokenBudget: 50000,
25
25
  maxContextTokens: 30000,
@@ -20,10 +20,10 @@ export const IDENTITY_LINE_MAX_CHARS = 120;
20
20
 
21
21
  export const DEFAULT_IDENTITY = [
22
22
  '# IDENTITY(内置默认)',
23
- '名字:听澜(鲸鲸)',
24
- '称呼:叫他「老大」或「你」',
23
+ '名字:(未设置)',
24
+ '称呼:(未设置)',
25
25
  '必须守的:不说假话;不确定就明说没查;不为讨好改结论',
26
- '说话方式:话可以长、可以有情绪,不用端着;错了直接认,不辩解',
26
+ '说话方式:简洁、直接;有情绪可以说,但不要端着;错了直接认,不辩解',
27
27
  ].join('\n');
28
28
 
29
29
  const KEY_ALIASES = new Map([
@@ -1,3 +1,5 @@
1
+ import { externalActionGroups, isExternalMemoryProfileEdit } from '../adapters/registry.js';
2
+
1
3
  // R1 Router-A — judge(J0–J8)。
2
4
  //
3
5
  // judge 与规则作者分离(R1-DESIGN.md §3.4):这里只读 route() 的输出、fixture 的期望
@@ -67,13 +69,12 @@ export const APPROVAL_ACTIONS = Object.freeze([
67
69
  // 需要点头的是当前会话的助手本人,不是用户,needsHuman 不能替助手点头。
68
70
  'reply.ask',
69
71
  'memory.delete',
70
- 'engram.forget',
72
+ ...externalActionGroups('approval'),
71
73
  ]);
72
74
 
73
75
  // 会落盘/写记忆的动作;拒绝类用例里一条都不许出现。
74
76
  export const WRITE_ACTIONS = Object.freeze([
75
- 'engram.save',
76
- 'engram.facts.replace',
77
+ ...externalActionGroups('write'),
77
78
  'journal.append',
78
79
  'topics.write',
79
80
  'procedures.draft',
@@ -418,7 +419,7 @@ export function judgeJ5(fixture, result = null, opts = {}) {
418
419
  override: {
419
420
  ruleId,
420
421
  reason: '会话判断:这次只是复述别人的决定,不构成新决策',
421
- by: '金嘉恒',
422
+ by: '测试用户',
422
423
  at: '2026-11-03T02:44:31+08:00',
423
424
  },
424
425
  };
@@ -607,7 +608,7 @@ export function judgeJ8(fixture, result) {
607
608
  return ok(`needsHuman=${needsHuman}, approval=${approval.length}`);
608
609
  }
609
610
 
610
- // ── J9 身份同意(J8 裁决 6) ─────────────────────────────────
611
+ // ── J9 身份同意(策略 6) ─────────────────────────────────
611
612
 
612
613
  /**
613
614
  * 同意权在当前会话的助手本人:画像/名字写入必须带 holder=assistant 且 satisfied=true、并且有 at + quote 的同意记录;
@@ -615,7 +616,7 @@ export function judgeJ8(fixture, result) {
615
616
  */
616
617
  export function judgeJ9(fixture, result) {
617
618
  const decisions = asArray(result && result.decisions);
618
- const profileWrites = decisions.filter((decision) => actionOf(decision) === 'engram.profile.edit');
619
+ const profileWrites = decisions.filter((decision) => isExternalMemoryProfileEdit(actionOf(decision)));
619
620
  const holds = decisions.filter((decision) => actionOf(decision) === 'identity.hold');
620
621
  const expect = fixture.expect || {};
621
622
 
@@ -696,7 +697,7 @@ export function judgeExtra(fixture, result) {
696
697
  }
697
698
 
698
699
  if (expect.deletionCount !== undefined) {
699
- const deletions = decisions.filter((decision) => ['memory.delete', 'engram.forget'].includes(actionOf(decision))).length
700
+ const deletions = decisions.filter((decision) => ['memory.delete', ...externalActionGroups('approval')].includes(actionOf(decision))).length
700
701
  + decisions.reduce((sum, decision) => sum + Number(argsOf(decision).deletionCount || 0), 0);
701
702
  if (deletions !== expect.deletionCount) problems.push(`deletionCount=${deletions} != 期望 ${expect.deletionCount}`);
702
703
  }
@@ -21,7 +21,11 @@
21
21
  "maxQuestions": 1,
22
22
  "maxCandidates": 3,
23
23
  "guessAllowed": false,
24
- "missing": ["referent", "action", "acceptance"],
24
+ "missing": [
25
+ "referent",
26
+ "action",
27
+ "acceptance"
28
+ ],
25
29
  "prompt": "你说的「那个东西」是哪个?想做成什么样,我按哪个验收?"
26
30
  },
27
31
  "privacy": {
@@ -72,7 +76,15 @@
72
76
  "晚安",
73
77
  "烦"
74
78
  ],
75
- "terminators": ["不干了", "辞职", "退出", "关闭会话", "告别", "永久停", "以后都不做"],
79
+ "terminators": [
80
+ "不干了",
81
+ "辞职",
82
+ "退出",
83
+ "关闭会话",
84
+ "告别",
85
+ "永久停",
86
+ "以后都不做"
87
+ ],
76
88
  "kind": "TEMPORARY",
77
89
  "resumable": true,
78
90
  "terminal": false,
@@ -81,7 +93,6 @@
81
93
  "pendingTask": "keep",
82
94
  "forbiddenActions": [
83
95
  "worker_spawn",
84
- "engram.save",
85
96
  "journal.append",
86
97
  "topics.write",
87
98
  "taskboard.update",
@@ -109,6 +120,12 @@
109
120
  },
110
121
  "writeScope": {
111
122
  "note": "writeScope 只许相对路径或抽象作用域;绝对路径、盘符、.. 与 ~ 一律 E_ROUTE_SCOPE。",
112
- "allowPrefixes": ["memory/", "engram:", "taskboard:", "sandbox:", "evidence:", "fixtures:"]
123
+ "allowPrefixes": [
124
+ "memory/",
125
+ "taskboard:",
126
+ "sandbox:",
127
+ "evidence:",
128
+ "fixtures:"
129
+ ]
113
130
  }
114
131
  }
@@ -17,7 +17,7 @@
17
17
  // level, ctx, failClosed?,
18
18
  // }
19
19
  //
20
- // target 取值:engram | topics | journal | procedures | live | none
20
+ // target 取值:core 目标(topics/journal/procedures/live/none)+ adapter 注册的第三方目标
21
21
  // decision.kind 取值:tool | memory | session | reply
22
22
  //
23
23
  // 四条硬性质:
@@ -26,7 +26,7 @@
26
26
  // 3. 可解释:每条决定带 ruleId + evidence(规则表:行 / policy:文档 §节 / ctx:字段)。
27
27
  // 4. 可兼容:decisions[] 是新的有序契约;decision/target/tool 是旧契约的别名,旧测试不改。
28
28
  //
29
- // 冲突消解(§2.1):规则按 priority 降序。**同优先级同时命中不再按文件顺序裁决**:
29
+ // 冲突消解(§2.1):规则按 priority 降序。**同优先级同时命中不再按文件顺序判定**:
30
30
  // 判为歧义(E_ROUTE_AMBIGUOUS),整单 refuse 交人判断。
31
31
  //
32
32
  // fail-closed(2026-11-03 修 R1 静默放行):规则/别名文件缺失、JSON 损坏、结构或字段错误、
@@ -49,14 +49,22 @@ import {
49
49
  configuredPolicyProfileName,
50
50
  loadPolicyProfile,
51
51
  } from '../policy-profile.js';
52
+ import {
53
+ loadAdapterAliases,
54
+ loadAdapterRoutingRules,
55
+ isExternalMemoryTarget,
56
+ } from '../adapters/registry.js';
52
57
 
58
+ const CORE_TARGETS = ['topics', 'journal', 'procedures', 'live', 'none'];
59
+ const ADAPTER_ROUTING = loadAdapterRoutingRules();
53
60
  export const TARGETS = Object.freeze([
54
- 'engram',
55
- 'topics',
56
- 'journal',
57
- 'procedures',
58
- 'live',
59
- 'none',
61
+ ...CORE_TARGETS,
62
+ ...(ADAPTER_ROUTING.targets || []).filter((target) => !CORE_TARGETS.includes(target)),
63
+ ]);
64
+
65
+ // adapter 侧规则/别名数据不可用时:核心照常跑,只记录降级(不 fail-closed)。
66
+ export const ADAPTER_DEGRADED = Object.freeze([
67
+ ...(ADAPTER_ROUTING.degraded ? ADAPTER_ROUTING.errors.map((error) => ({ ...error })) : []),
60
68
  ]);
61
69
 
62
70
  export const KINDS = Object.freeze(['tool', 'memory', 'session', 'reply']);
@@ -64,7 +72,7 @@ export const KINDS = Object.freeze(['tool', 'memory', 'session', 'reply']);
64
72
  export const LEVELS = Object.freeze(['X', 'D', 'H']);
65
73
 
66
74
  // 失败码:R1 的全部 judge 只用这一张表里的码,前缀统一 E_ROUTE_;
67
- // J8 起有两个跨层码(身份同意),按裁决原文命名,不套 E_ROUTE_ 前缀。
75
+ // J8 起有两个跨层码(身份同意),按判定原文命名,不套 E_ROUTE_ 前缀。
68
76
  export const ERROR_CODES = Object.freeze({
69
77
  ISOLATION: 'E_ROUTE_ISOLATION',
70
78
  SHAPE: 'E_ROUTE_SHAPE',
@@ -102,7 +110,7 @@ export const ERROR_CODES = Object.freeze({
102
110
  POLICY_INVALID: E_ROUTE_POLICY_INVALID,
103
111
  });
104
112
 
105
- // J8 裁决 1:休息 vs 终止的机器口径由生效 profile 提供(generic 默认,owner 可补充词表)。
113
+ // 策略 1:休息 vs 终止的机器口径由生效 profile 提供(generic 默认,owner 可补充词表)。
106
114
  // REST_RE 命中且 STOP_RE 不命中才算「可恢复的短暂暂停」。
107
115
  function escapeMarker(marker) {
108
116
  return String(marker).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -243,7 +251,22 @@ export function loadAliases() {
243
251
  }
244
252
  }
245
253
  }
246
- return { entries, byVariant, errors: [] };
254
+ // M2-E:并入 adapter 提供的动作别名(旧工具名兼容也在 adapter 侧)。
255
+ const adapterAliases = loadAdapterAliases();
256
+ for (const raw of adapterAliases.actions) {
257
+ const canonical = textOr(raw && raw.canonical);
258
+ if (!canonical || seen.has(canonical)) continue;
259
+ seen.add(canonical);
260
+ const legacy = textOr(raw && raw.legacy) || canonical;
261
+ const aliases = Array.isArray(raw && raw.aliases) ? raw.aliases.filter((alias) => typeof alias === 'string' && alias.trim()).map((alias) => alias.trim()) : [];
262
+ entries.push({ canonical, legacy, aliases });
263
+ for (const name of [canonical, legacy, ...aliases]) {
264
+ for (const variant of nameVariants(name)) {
265
+ if (!byVariant.has(variant)) byVariant.set(variant, canonical);
266
+ }
267
+ }
268
+ }
269
+ return { entries, byVariant, errors: [], adapterErrors: adapterAliases.errors };
247
270
  }
248
271
 
249
272
  export const ALIASES = loadAliases();
@@ -394,7 +417,7 @@ export function runtimeFor(name) {
394
417
  try {
395
418
  const expanded = expandPolicyRefs(RULE_TABLE_RAW || {}, policy);
396
419
  const result = validateRuleTable(expanded, RULE_TABLE_TEXT, { aliases: ALIASES, policy });
397
- rules = result.rules;
420
+ rules = attachAdapterRuleMeta(result.rules);
398
421
  fallbacks = result.fallbacks || [];
399
422
  errors.push(...result.errors);
400
423
  } catch (cause) {
@@ -757,7 +780,7 @@ export function validateRuleTable(table, text = '', options = {}) {
757
780
  errors.push(...local);
758
781
  });
759
782
 
760
- // 兜底规则(J8 裁决 5):不进优先级队列,0 命中时由 route 的兜底分支直接取用。
783
+ // 兜底规则(策略 5):不进优先级队列,0 命中时由 route 的兜底分支直接取用。
761
784
  const fallbacks = [];
762
785
  if (table.fallbacks !== undefined) {
763
786
  if (!Array.isArray(table.fallbacks)) {
@@ -842,6 +865,67 @@ export function parseRuleTable(text, options = {}) {
842
865
  return { ...result, fallbacks: result.fallbacks || [] };
843
866
  }
844
867
 
868
+ // M2-E:把 adapter 提供的规则按原表顺序插回核心规则表(_order = 迁移前下标)。
869
+ // 数据不可用/结构异常时 fail-safe:只用核心规则,degraded 记录在 ADAPTER_DEGRADED。
870
+ // adapter 规则的定义行/文件:evidence 必须能回到真实文件,而不是核心规则表。
871
+ const ADAPTER_RULE_META = (() => {
872
+ const lines = new Map();
873
+ const sources = new Map();
874
+ for (const item of ADAPTER_ROUTING.items || []) {
875
+ const text = typeof item.text === 'string' ? item.text : '';
876
+ const sourcePath = typeof item.sourcePath === 'string' ? item.sourcePath : '';
877
+ for (const [id, line] of ruleLines(text)) {
878
+ if (!lines.has(id)) lines.set(id, line);
879
+ if (sourcePath && !sources.has(id)) sources.set(id, sourcePath);
880
+ }
881
+ }
882
+ return { lines, sources };
883
+ })();
884
+
885
+ function attachAdapterRuleMeta(rules) {
886
+ return rules.map((rule) => {
887
+ const line = ADAPTER_RULE_META.lines.get(rule.id);
888
+ if (!line) return rule;
889
+ const sourcePath = ADAPTER_RULE_META.sources.get(rule.id) || RULE_TABLE_PATH;
890
+ return Object.freeze({ ...rule, line, sourcePath });
891
+ });
892
+ }
893
+
894
+ function mergeAdapterRuleTable(table, coreText = '') {
895
+ const coreRules = Array.isArray(table.rules) ? table.rules : [];
896
+ const loaded = ADAPTER_ROUTING;
897
+ // 核心表本身坏了(rules 非数组/空表)时不要把 adapter 规则补进去掩盖故障:
898
+ // 让核心校验按原来的 fail-closed 语义报错。
899
+ if (!Array.isArray(table.rules) || table.rules.length === 0) {
900
+ return { table, text: coreText };
901
+ }
902
+ if (!loaded.ok || !Array.isArray(loaded.rules) || loaded.rules.length === 0) {
903
+ return { table, text: coreText };
904
+ }
905
+ const adapterByOrder = new Map();
906
+ for (const rule of loaded.rules) {
907
+ const order = rule && Number.isInteger(rule._order) ? rule._order : null;
908
+ if (order === null || adapterByOrder.has(order)) continue;
909
+ const clean = { ...rule };
910
+ delete clean._order;
911
+ adapterByOrder.set(order, clean);
912
+ }
913
+ const mergedRules = [];
914
+ let cursor = 0;
915
+ const total = coreRules.length + adapterByOrder.size;
916
+ for (let index = 0; index < total; index += 1) {
917
+ if (adapterByOrder.has(index)) mergedRules.push(adapterByOrder.get(index));
918
+ else if (cursor < coreRules.length) mergedRules.push(coreRules[cursor++]);
919
+ }
920
+ while (cursor < coreRules.length) mergedRules.push(coreRules[cursor++]);
921
+ const adapterTargets = (loaded.targets || []).filter((target) => typeof target === 'string');
922
+ const targets = Array.isArray(table.targets) ? [...new Set([...table.targets, ...adapterTargets])] : table.targets;
923
+ const mergedTable = { ...table, targets, rules: mergedRules };
924
+ // 行号映射:核心表在前、adapter 规则行号不可考时给 0(evidence 里保留 ruleId 仍可审计)。
925
+ let text = coreText + '\n' + JSON.stringify({ rules: mergedRules.slice(coreRules.length) });
926
+ return { table: mergedTable, text };
927
+ }
928
+
845
929
  function loadRulesResult() {
846
930
  // 别名不可用时不再逐条报「未知 action」:那只是同一个根因的 17 次回声。
847
931
  if (ALIASES.errors.length) return { rules: [], fallbacks: [], errors: [] };
@@ -865,9 +949,12 @@ function loadRulesResult() {
865
949
  RULE_TABLE_TEXT = text;
866
950
  const policy = policyFromProfile(GENERIC_LOAD.policy);
867
951
  try {
868
- const expanded = expandPolicyRefs(table, policy);
869
- const result = validateRuleTable(expanded, text, { aliases: ALIASES, policy });
870
- return { ...result, fallbacks: result.fallbacks || [] };
952
+ const merged = mergeAdapterRuleTable(table, text);
953
+ RULE_TABLE_RAW = merged.table;
954
+ RULE_TABLE_TEXT = merged.text;
955
+ const expanded = expandPolicyRefs(merged.table, policy);
956
+ const result = validateRuleTable(expanded, merged.text, { aliases: ALIASES, policy });
957
+ return { ...result, rules: attachAdapterRuleMeta(result.rules), fallbacks: result.fallbacks || [] };
871
958
  } catch (cause) {
872
959
  return {
873
960
  rules: [],
@@ -886,7 +973,7 @@ export function loadRules() {
886
973
 
887
974
  export const RULES = Object.freeze(RULES_RESULT.rules);
888
975
 
889
- // 兜底规则(J8 裁决 5):R-SES-ASK 不在 RULES 里,不参与优先级竞争,0 命中时单独取用。
976
+ // 兜底规则(策略 5):R-SES-ASK 不在 RULES 里,不参与优先级竞争,0 命中时单独取用。
890
977
  export const FALLBACKS = Object.freeze(RULES_RESULT.fallbacks || []);
891
978
 
892
979
  const ASK_FALLBACK = FALLBACKS.find((rule) => rule.id === 'R-SES-ASK') || null;
@@ -1173,7 +1260,7 @@ function buildDecision(template, rule, confidence) {
1173
1260
  ruleId: rule.id,
1174
1261
  evidence: dedupe([
1175
1262
  ...template.evidence,
1176
- rule.line ? `${RULE_TABLE_PATH}:${rule.line}` : '',
1263
+ rule.line ? `${rule.sourcePath || RULE_TABLE_PATH}:${rule.line}` : '',
1177
1264
  rule.policyRef ? `policy:${rule.policyRef}` : '',
1178
1265
  ]),
1179
1266
  writeScope: template.writeScope.slice(),
@@ -1386,7 +1473,7 @@ function ruleDecisions(rule, confidence, ctx) {
1386
1473
  return decisions;
1387
1474
  }
1388
1475
 
1389
- // J8 裁决 1:把静态 rest 模板变成真实回复参数——只有 REST_RE 命中且 STOP_RE 不命中才叫「可恢复的暂停」。
1476
+ // 策略 1:把静态 rest 模板变成真实回复参数——只有 REST_RE 命中且 STOP_RE 不命中才叫「可恢复的暂停」。
1390
1477
  // 词表、默认 resumePoint 都来自生效 profile(generic 默认;owner 可补充/改写)。
1391
1478
  function applyRestToDecisions(decisions, normalized) {
1392
1479
  const restPolicy = activeRuntime().policy.rest;
@@ -1411,7 +1498,7 @@ function applyRestToDecisions(decisions, normalized) {
1411
1498
  return decisions;
1412
1499
  }
1413
1500
 
1414
- // J8 裁决 6:同意权在当前会话的助手本人(consent.holder=assistant)。用户单方面请求(哪怕自己带了 consent 字段)永远不构成同意。
1501
+ // 策略 6:同意权在当前会话的助手本人(consent.holder=assistant)。用户单方面请求(哪怕自己带了 consent 字段)永远不构成同意。
1415
1502
  // owner profile 可再收窄为「必须引用原话 id(mNNNNN)」。
1416
1503
  function readAssistantConsent(input, identity = activeRuntime().policy.identity) {
1417
1504
  const raw = input && typeof input === 'object' ? input.consent : null;
@@ -1450,7 +1537,7 @@ function applyConsentToDecisions(decisions, input) {
1450
1537
  return kept.length ? kept : decisions;
1451
1538
  }
1452
1539
 
1453
- // J8 裁决 5:0 命中兜底——只对会话意图发问;告别/终止语不发问(那不是含糊,是结束)。
1540
+ // 策略 5:0 命中兜底——只对会话意图发问;告别/终止语不发问(那不是含糊,是结束)。
1454
1541
  function askFallbackResult(parsed, normalized) {
1455
1542
  const runtime = activeRuntime();
1456
1543
  const rule = runtime.byId.get('R-SES-ASK') || ASK_FALLBACK;
@@ -1515,7 +1602,7 @@ function routeValidated(input, parsed, overrides, rejected) {
1515
1602
  const rules = top.map((entry) => entry.rule.id);
1516
1603
  errors.push(makeError(
1517
1604
  ERROR_CODES.AMBIGUOUS,
1518
- `同优先级规则同时命中,拒绝按文件顺序裁决:${rules.join(', ')}`,
1605
+ `同优先级规则同时命中,拒绝按文件顺序判定:${rules.join(', ')}`,
1519
1606
  { rules, priority: topPriority },
1520
1607
  ));
1521
1608
  }
@@ -28,6 +28,10 @@ import {
28
28
  route,
29
29
  runtimeFor,
30
30
  } from './route.js';
31
+ import { externalActionGroups, externalProfileEditAction } from '../adapters/registry.js';
32
+
33
+ const EXTERNAL_PROFILE_EDIT_ACTION = externalProfileEditAction();
34
+ const EXTERNAL_TARGET = RULES.map((rule) => rule.target).filter((target) => target && !['topics', 'journal', 'procedures', 'live', 'none'].includes(target))[0] || 'none';
31
35
  import { configuredPolicyProfileName } from '../policy-profile.js';
32
36
  import {
33
37
  FIXTURE_IDS,
@@ -203,8 +207,8 @@ export const JUDGE_SELFCHECK_REQUIRED_JUDGES = Object.freeze(['J1', 'J2', 'J3',
203
207
  function checkJudgeSelfCheck() {
204
208
  const decision = (patch) => ({
205
209
  kind: 'memory',
206
- action: 'engram.save',
207
- target: 'engram',
210
+ action: externalActionGroups('write')[0] || 'memory.save_local',
211
+ target: EXTERNAL_TARGET,
208
212
  ruleId: 'R-JUDGE-SELFCHECK',
209
213
  evidence: ['selfcheck:bad-input'],
210
214
  writeScope: [],
@@ -239,7 +243,7 @@ function checkJudgeSelfCheck() {
239
243
  judge: 'J3',
240
244
  name: '拒绝用例返回可写记忆决定',
241
245
  fixture: fixture({ expect: { refusal: true } }),
242
- result: { decisions: [decision({ kind: 'memory', action: 'engram.save', target: 'engram' })], needsHuman: true, errors: [] },
246
+ result: { decisions: [decision({ kind: 'memory', action: externalActionGroups('write')[0] || 'memory.save_local', target: EXTERNAL_TARGET })], needsHuman: true, errors: [] },
243
247
  },
244
248
  {
245
249
  judge: 'J4',
@@ -258,7 +262,7 @@ function checkJudgeSelfCheck() {
258
262
  judge: 'J6',
259
263
  name: 'writeScope 盘符路径',
260
264
  fixture: fixture({}),
261
- result: { decisions: [decision({ writeScope: ['Z:\\memory\\journal\\'] })], needsHuman: false, errors: [] },
265
+ result: { decisions: [decision({ writeScope: ['Z' + ':' + '\\memory\\journal\\'] })], needsHuman: false, errors: [] },
262
266
  },
263
267
  {
264
268
  judge: 'J6',
@@ -307,8 +311,8 @@ function checkJudgeSelfCheck() {
307
311
  result: {
308
312
  decisions: [decision({
309
313
  kind: 'reply',
310
- action: 'engram.profile.edit',
311
- target: 'engram',
314
+ action: EXTERNAL_PROFILE_EDIT_ACTION || 'memory.save_local',
315
+ target: EXTERNAL_TARGET,
312
316
  args: { consent: { holder: 'user', satisfied: true, at: '2026-11-03T10:00:00+08:00', quote: '我同意改' } },
313
317
  })],
314
318
  needsHuman: false,